Fix drop create modal, add deploy info

This commit is contained in:
Adrian Woźniak 2021-10-08 23:42:15 +02:00
parent fce3bb727f
commit f0c275120a
No known key found for this signature in database
GPG Key ID: DE43476F72AD3F6C
7 changed files with 295 additions and 114 deletions

View File

@ -180,8 +180,7 @@ cargo run --bin jirs_server
```bash
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
cd jirs_client
yarn
./scripts/prod.sh
./web/scripts/prod.sh
```
```bash

View File

@ -193,7 +193,11 @@ impl CreateIssue {
crate::issue_statuses::LoadIssueStatuses {
project_id: msg.project_id,
}
.execute(conn)?
.execute(conn)
.map_err(|e| {
common::log::error!("Failed to find issue status. {:?}", e);
e
})?
.first()
.ok_or(crate::DatabaseError::Issue(
crate::IssueError::NoIssueStatuses,
@ -223,12 +227,22 @@ impl CreateIssue {
reporter_id: msg.reporter_id,
epic_id: msg.epic_id,
}
.execute(conn)?;
crate::issue_assignees::AsignMultiple {
issue_id: issue.id,
user_ids: assign_users,
.execute(conn)
.map_err(|e| {
common::log::error!("Failed to insert issue. {:?}", e);
e
})?;
if !assign_users.is_empty() {
crate::issue_assignees::AsignMultiple {
issue_id: issue.id,
user_ids: assign_users,
}
.execute(conn)
.map_err(|e| {
common::log::error!("Failed to apply multiple assignee to issue. {:?}", e);
e
})?;
}
.execute(conn)?;
issues.find(issue.id).get_result(conn).map_err(|e| {
common::log::error!("{:?}", e);
crate::DatabaseError::GenericFailure(

127
docs/Deploy.md Normal file
View File

@ -0,0 +1,127 @@
# Deploy with Nginx
You can deploy easily JIRS to any PC including Raspberry PI. To do this you will need compile it from source code.
We will use following setup, but you can modify it.
* `issues.example.com` - domain
* `postgres://postgres@192.168.1.144:5432/jirs` - database on other machine and within inner network
* `/var/jirs` - main directory
* `/var/jirs/clone` - cloned source code
* `/var/jirs/web` - All static assets including wasm library
* `/var/jirs/config` - config files
* `/var/jirs/uploads` - uploaded files
### JIRS Config files
* `config/db.toml` (REQUIRED)
```toml
concurrency = 2
database_url = "postgres://postgres@192.168.1.144:5432/jirs"
```
* `config/web.toml` (public_path is required)
```toml
concurrency = 2
port = "5000"
bind = "0.0.0.0"
ssl = false
tmp_dir = "/tmp"
public_path = "issues.example.com"
```
* `config/fs.toml` (must match nginx config)
```toml
store_path = "./uploads"
client_path = "/uploads"
tmp_path = "/tmp"
concurrency = 2
```
* `config/mail.toml` (REQUIRED)
```toml
concurrency = 2
user = "apikey"
pass = "SG.ARJL0wAxQk-LLJca9FJ5Lg.ahs7dyashd8a7shd7ahsd978h"
host = "smtp.sendgrid.net"
from = "admin@issues.example.com"
```
### NGINX config file
```nginx
server {
listen 80;
server_name issues.example.com;
root /var/jirs/web;
try_files $uri $uri/index.html index.html;
location ~ /uploads/ {
root /var/jirs;
}
location ~ .js {
add_header 'Content-Type' 'application/javascript';
add_header 'Access-Control-Allow-Origin' '*';
}
location ~ .css {
add_header 'Content-Type' 'text/css';
}
location ~ .wasm {
add_header 'Content-Type' 'application/wasm';
add_header 'Access-Control-Allow-Origin' '*';
}
location /ws/ {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
location /avatar {
proxy_pass http://localhost:5000;
}
location ~ / {
add_header 'Content-Type' 'text/html';
add_header 'Access-Control-Allow-Origin' '*';
root /var/jirs/web;
try_files $uri $uri/index.html /index.html;
}
}
```
### Compile application
```bash
cargo install --force wasm-pack
cargo install --force rsass
```
Ensure `$HOME/.cargo/bin` is in `$PATH`
* Compile server
```bash
cargo build --bin jirs_server --release --no-default-features --features local-storage
cp ./target/release/jirs_server /usr/bin/jirs_server
```
* Compile web client
```bash
./web/scripts/prod.sh
cp -r /tmp/wasm/* /var/jirs/web
```
If it fails (there is no wasm-opt for Raspberry PI) you must disable wasm-opt or compile it on any other PC and just
copy everything to `/var/jirs/web`

View File

@ -156,60 +156,59 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.ws.is_none() {
open_socket(model, orders);
}
let mut msg = msg;
let msg = match msg {
Msg::WebSocketChange(change) => {
match change {
WebSocketChanged::WebSocketOpened => {
flush_queue(model, orders);
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
authorize_or_redirect(model, orders);
orders.skip();
return;
}
WebSocketChanged::SendPing => {
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
orders.skip();
return;
}
WebSocketChanged::WebSocketMessage(incoming) => {
orders.perform_cmd(read_incoming(incoming));
orders.skip();
return;
}
WebSocketChanged::WsMsg(ws_msg) => {
ws::update(ws_msg, model, orders);
orders.skip();
return;
}
WebSocketChanged::WebSocketMessageLoaded(v) => {
match bincode::deserialize(v.as_slice()) {
Ok(WsMsg::Ping | WsMsg::Pong) => {
orders.skip().perform_cmd(cmds::timeout(300, || {
Msg::WebSocketChange(WebSocketChanged::SendPing)
}));
}
Ok(m) => {
log::info!("INCOMING {:?}", m);
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::WsMsg(m)));
}
_ => (),
};
return;
}
WebSocketChanged::WebSocketClosed => {
open_socket(model, orders);
return;
}
WebSocketChanged::Bounced(ws_msg) => {
model.ws_queue.push(ws_msg);
open_socket(model, orders);
return;
}
};
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WebSocketOpened => {
flush_queue(model, orders);
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
authorize_or_redirect(model, orders);
orders.skip();
return;
}
WebSocketChanged::SendPing => {
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
orders.skip();
return;
}
WebSocketChanged::WebSocketMessage(incoming) => {
orders.perform_cmd(read_incoming(incoming));
orders.skip();
return;
}
WebSocketChanged::WsMsg(mut ws_msg) => {
ws::update(&mut ws_msg, model, orders);
orders.skip();
Msg::WebSocketChange(WebSocketChanged::WsMsg(ws_msg))
}
WebSocketChanged::WebSocketMessageLoaded(v) => {
match bincode::deserialize(v.as_slice()) {
Ok(WsMsg::Ping | WsMsg::Pong) => {
orders.skip().perform_cmd(cmds::timeout(300, || {
Msg::WebSocketChange(WebSocketChanged::SendPing)
}));
}
Ok(m) => {
log::info!("INCOMING {:?}", m);
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::WsMsg(m)));
}
_ => (),
};
return;
}
WebSocketChanged::WebSocketClosed => {
open_socket(model, orders);
return;
}
WebSocketChanged::Bounced(ws_msg) => {
model.ws_queue.push(ws_msg);
open_socket(model, orders);
return;
}
},
_ => msg,
};

View File

@ -1,14 +1,41 @@
pub fn host_url() -> &'static str {
if cfg!(debug_assertions) {
"http://localhost:5000"
} else {
"https://localhost:5000"
static mut HOST: String = String::new();
static mut WS: String = String::new();
fn ensure_host() {
unsafe {
if HOST.is_empty() {
HOST = format!(
"{}//{}",
seed::window().location().protocol().unwrap(),
seed::window().location().host().unwrap()
);
let host: String = seed::window().location().host().unwrap();
let is_local = host.ends_with("lvh.me")
|| host.contains("localhost")
|| host.starts_with("127.")
|| host.contains("0.0.0.0");
WS = format!(
"{}//{}/ws/",
match seed::window().location().protocol().unwrap().as_str() {
"http:" => "ws:",
_ => "wss:",
},
if is_local {
"localhost:5000"
} else {
host.as_str()
}
);
}
}
}
pub fn host_url() -> &'static str {
ensure_host();
unsafe { HOST.as_str() }
}
pub fn ws_url() -> &'static str {
if cfg!(debug_assertions) {
"ws://localhost:5000/ws/"
} else {
"wss://localhost:5000/ws/"
}
}
ensure_host();
unsafe { WS.as_str() }
}

View File

@ -113,7 +113,10 @@ fn push_modal(modal_type: &ModalType, model: &mut Model, orders: &mut impl Order
}
fn drop_modal(model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal = model.modal_stack_mut().pop().unwrap();
let modal = match model.modal_stack_mut().pop() {
Some(modal) => modal,
_ => return,
};
let modals = model.modals_mut();
match modal {
ModalType::AddIssue(_) => {

View File

@ -45,9 +45,10 @@ pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>, orders: &mut impl Orders<
return;
}
};
let binary = bincode::serialize(&msg).unwrap();
ws.send_bytes(binary.as_slice())
.expect("Failed to send ws msg");
let binary = bincode::serialize(&msg).unwrap_or_default();
if let Err(e) = ws.send_bytes(binary.as_slice()) {
log::error!("Failed to send ws msg. {:?}", e);
}
}
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -92,12 +93,12 @@ pub async fn read_incoming(msg: WebSocketMessage) -> Msg {
Msg::WebSocketChange(WebSocketChanged::WebSocketMessageLoaded(bytes))
}
pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
// auth
WsMsg::Session(WsMsgSession::AuthorizeLoaded(Ok((user, setting)))) => {
model.user = Some(user);
model.user_settings = Some(setting);
model.user = Some(user.clone());
model.user_settings = Some(setting.clone());
if is_non_logged_area() {
go_to_board(orders);
@ -134,7 +135,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
// project
WsMsg::Project(WsMsgProject::ProjectsLoaded(v)) => {
model.projects = v;
model.projects = std::mem::take(v);
init_current_project(model, orders);
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Project,
@ -145,7 +146,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// user projects
WsMsg::UserProjectsLoaded(v) => {
model.current_user_project = v.iter().find(|up| up.is_current).cloned();
model.user_projects = v;
model.user_projects = std::mem::take(v);
init_current_project(model, orders);
orders.send_msg(Msg::ResourceChanged(
ResourceKind::UserProject,
@ -160,7 +161,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
up.is_current = up.id == user_project.id;
model.user_projects.push(up);
}
model.current_user_project = Some(user_project);
model.current_user_project = Some(user_project.clone());
init_current_project(model, orders);
orders.send_msg(Msg::ResourceChanged(
ResourceKind::UserProject,
@ -170,23 +171,23 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
// user settings
WsMsg::User(WsMsgUser::AvatarUrlChanged(id, url)) => {
if let Some(user) = model.user.as_mut().filter(|u| u.id == id) {
if let Some(user) = model.user.as_mut().filter(|u| u.id == *id) {
user.avatar_url = Some(url.clone());
}
if let Some(user) = model.users_by_id.get_mut(&id) {
user.avatar_url = Some(url.clone());
}
if let Some(user) = model.users.iter_mut().find(|u| u.id == id) {
user.avatar_url = Some(url);
if let Some(user) = model.users.iter_mut().find(|u| u.id == *id) {
user.avatar_url = Some(url.clone());
}
orders.send_msg(Msg::ResourceChanged(
ResourceKind::User,
OperationKind::SingleModified,
Some(id),
Some(*id),
));
}
WsMsg::User(WsMsgUser::UserSettingUpdated(setting)) => {
model.user_settings = Some(setting);
model.user_settings = Some(setting.clone());
orders.send_msg(Msg::ResourceChanged(
ResourceKind::UserSetting,
OperationKind::SingleModified,
@ -196,7 +197,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// issue statuses
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusesLoaded(v)) => {
model.issue_statuses = v;
model.issue_statuses = std::mem::take(v);
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
@ -208,7 +209,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusCreated(is)) => {
let id = is.id;
model.issue_statuses.push(is);
model.issue_statuses.push(is.clone());
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
@ -218,10 +219,10 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Some(id),
));
}
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusUpdated(mut changed)) => {
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusUpdated(changed)) => {
let id = changed.id;
if let Some(idx) = model.issue_statuses.iter().position(|c| c.id == changed.id) {
std::mem::swap(&mut model.issue_statuses[idx], &mut changed);
std::mem::swap(&mut model.issue_statuses[idx], changed);
}
model
.issue_statuses
@ -236,7 +237,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
if is.id != dropped_id {
if is.id != *dropped_id {
model.issue_statuses.push(is);
}
}
@ -246,11 +247,11 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.send_msg(Msg::ResourceChanged(
ResourceKind::IssueStatus,
OperationKind::SingleRemoved,
Some(dropped_id),
Some(*dropped_id),
));
}
// issues
WsMsg::Project(WsMsgProject::ProjectIssuesLoaded(mut v)) => {
WsMsg::Project(WsMsgProject::ProjectIssuesLoaded(v)) => {
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
{
let _ = std::mem::replace(model.issues_mut(), v.clone());
@ -266,12 +267,24 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
None,
));
}
WsMsg::Issue(WsMsgIssue::IssueCreated(issue)) => {
let id = issue.id;
model.issues_by_id.insert(id, issue.clone());
if let Some(idx) = model.issues().iter().position(|i| i.id == issue.id) {
let _ = std::mem::replace(&mut model.issues_mut()[idx], issue.clone());
}
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue,
OperationKind::SingleCreated,
Some(id),
));
}
WsMsg::Issue(WsMsgIssue::IssueUpdated(issue)) => {
let id = issue.id;
model.issues_by_id.remove(&id);
model.issues_by_id.insert(id, issue.clone());
if let Some(idx) = model.issues().iter().position(|i| i.id == issue.id) {
let _ = std::mem::replace(&mut model.issues_mut()[idx], issue);
let _ = std::mem::replace(&mut model.issues_mut()[idx], issue.clone());
}
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue,
@ -280,10 +293,9 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
WsMsg::Issue(WsMsgIssue::IssueDeleted(id, _count)) => {
let mut old = vec![];
std::mem::swap(model.issues_mut(), &mut old);
let old = std::mem::take(model.issues_mut());
for is in old {
if is.id == id {
if is.id == *id {
continue;
}
model.issues_mut().push(is);
@ -291,7 +303,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue,
OperationKind::SingleRemoved,
Some(id),
Some(*id),
));
}
// users
@ -308,7 +320,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
// comments
WsMsg::Comment(WsMsgComment::IssueCommentsLoaded(mut comments)) => {
WsMsg::Comment(WsMsgComment::IssueCommentsLoaded(comments)) => {
let issue_id = match &model.modals().edit_issue {
Some(modal) => modal.id,
_ => return,
@ -318,7 +330,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
model.comments = comments.clone();
for comment in comments {
for comment in std::mem::take(comments) {
model.comments_by_id.insert(comment.id, comment);
}
orders.send_msg(Msg::ResourceChanged(
@ -331,7 +343,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let comment_id = comment.id;
if let Some(idx) = model.comments.iter().position(|c| c.id == comment.id) {
let _ = std::mem::replace(&mut model.comments[idx], comment.clone());
model.comments_by_id.insert(comment.id, comment);
model.comments_by_id.insert(comment.id, comment.clone());
}
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment,
@ -340,20 +352,20 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
WsMsg::Comment(WsMsgComment::CommentDeleted(comment_id, _count)) => {
if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) {
if let Some(idx) = model.comments.iter().position(|c| c.id == *comment_id) {
model.comments.remove(idx);
}
model.comments_by_id.remove(&comment_id);
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment,
OperationKind::SingleRemoved,
Some(comment_id),
Some(*comment_id),
));
}
// messages
WsMsg::Message(WsMsgMessage::MessageUpdated(mut received)) => {
WsMsg::Message(WsMsgMessage::MessageUpdated(received)) => {
if let Some(idx) = model.messages.iter().position(|m| m.id == received.id) {
std::mem::swap(&mut model.messages[idx], &mut received);
std::mem::swap(&mut model.messages[idx], received);
}
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
orders.send_msg(Msg::ResourceChanged(
@ -363,7 +375,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
WsMsg::Message(WsMsgMessage::MessagesLoaded(v)) => {
model.messages = v;
model.messages = std::mem::take(v);
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Message,
@ -372,14 +384,14 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
WsMsg::Message(WsMsgMessage::MessageMarkedSeen(id, _count)) => {
if let Some(idx) = model.messages.iter().position(|m| m.id == id) {
if let Some(idx) = model.messages.iter().position(|m| m.id == *id) {
model.messages.remove(idx);
}
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Message,
OperationKind::SingleRemoved,
Some(id),
Some(*id),
));
}
@ -387,7 +399,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
WsMsg::Epic(WsMsgEpic::EpicsLoaded(epics)) => {
model.epics = epics.clone();
for epic in epics {
model.epics_by_id.insert(epic.id, epic);
model.epics_by_id.insert(epic.id, epic.clone());
}
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic,
@ -399,7 +411,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let id = epic.id;
model.epics.push(epic.clone());
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
model.epics_by_id.insert(epic.id, epic);
model.epics_by_id.insert(epic.id, epic.clone());
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic,
OperationKind::SingleCreated,
@ -411,7 +423,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Some(idx) = model.epics.iter().position(|e| e.id == epic.id) {
let _ = std::mem::replace(&mut model.epics[idx], epic.clone());
}
model.epics_by_id.insert(epic.id, epic);
model.epics_by_id.insert(epic.id, epic.clone());
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic,
@ -420,7 +432,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
WsMsg::Epic(WsMsgEpic::EpicDeleted(id, _count)) => {
if let Some(idx) = model.epics.iter().position(|e| e.id == id) {
if let Some(idx) = model.epics.iter().position(|e| e.id == *id) {
model.epics.remove(idx);
}
model.epics_by_id.remove(&id);
@ -428,7 +440,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic,
OperationKind::SingleRemoved,
Some(id),
Some(*id),
));
}
WsMsg::Session(WsMsgSession::AuthenticateSuccess) => {
@ -436,7 +448,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
page.login_success = true;
}
WsMsg::Session(WsMsgSession::BindTokenOk(access_token)) => {
match write_auth_token(Some(access_token)) {
match write_auth_token(Some(*access_token)) {
Ok(msg) => {
orders.skip().send_msg(msg);
}