chore: cleanup, node14 and new admin default
This commit is contained in:
parent
c7c18633d7
commit
89bb9a8f25
244 changed files with 3957 additions and 39487 deletions
0
.dummy
0
.dummy
|
|
@ -1 +0,0 @@
|
|||
{"migrations":[{"title":"001-initial.js"},{"title":"002-bills.js"},{"title":"003-device-events.js"},{"title":"004-transactions-reload.js"},{"title":"005-addCrypto.js"},{"title":"006-add-machine-config.js"},{"title":"007-add-phone.js"},{"title":"008-add-two-way.js"},{"title":"009-update-timestamps.js"},{"title":"010-cached-requests.js"},{"title":"011-transactions-reload-2.js"},{"title":"012-add-hd-path-serial.js"},{"title":"013-add-last-checked.js"},{"title":"014-session-to-tx-id.js"},{"title":"015-paired_devices.js"},{"title":"016-new_cached_requests_table.js"},{"title":"017-user_tokens.js"},{"title":"018-alter_devices.js"},{"title":"019-remove-dispense-counts.js"},{"title":"020-add-server-events.js"},{"title":"021-config-version-id.js"},{"title":"022-add_cash_in_sent.js"},{"title":"023-add-dispenses-to-cash-out.js"},{"title":"024-consolidate-hd.js"},{"title":"025-create_trades.js"},{"title":"026-add_send_confirmed.js"},{"title":"027-tx_errors.js"},{"title":"028-cash_out_actions.js"},{"title":"029-add_valid_to_user_config.js"},{"title":"030-cash-out-provision.js"},{"title":"031-remove_name_from_devices.js"},{"title":"032-create_machine_pings_table.js"},{"title":"033-add_cash_in_fee.js"},{"title":"034-add_cash_out_error_code.js"},{"title":"035-log_bank_notes.js"}],"path":"/Users/josh/projects/lamassu-server/.migrate-stress","pos":35}
|
||||
|
|
@ -42,11 +42,10 @@ Notes:
|
|||
|
||||
## Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway. Also, ignore Debug lines from lamassu-apply-defaults.
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node bin/lamassu-migrate
|
||||
node bin/lamassu-apply-defaults
|
||||
```
|
||||
|
||||
## Register admin user
|
||||
|
|
|
|||
|
|
@ -60,11 +60,10 @@ Notes:
|
|||
|
||||
## Set up database
|
||||
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway. Also, ignore Debug lines from lamassu-apply-defaults.
|
||||
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway.
|
||||
|
||||
```
|
||||
node bin/lamassu-migrate
|
||||
node bin/lamassu-apply-defaults
|
||||
```
|
||||
|
||||
## Register admin user
|
||||
|
|
|
|||
1
Procfile
1
Procfile
|
|
@ -1 +0,0 @@
|
|||
web: node lib/app.js
|
||||
36
TODO.json
36
TODO.json
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"groups": [
|
||||
{
|
||||
"code": "balanceAlerts",
|
||||
"fields": [
|
||||
"cashInAlertThreshold"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "compliance",
|
||||
"fields": [
|
||||
"smsVerificationActive",
|
||||
"smsVerificationThreshold",
|
||||
"idCardDataVerificationActive",
|
||||
"idCardDataVerificationThreshold",
|
||||
"idCardPhotoVerificationActive",
|
||||
"idCardPhotoVerificationThreshold",
|
||||
"sanctionsVerificationActive",
|
||||
"sanctionsVerificationThreshold",
|
||||
"frontCameraVerificationActive",
|
||||
"frontCameraVerificationThreshold",
|
||||
"hardLimitVerificationActive",
|
||||
"hardLimitVerificationThreshold",
|
||||
"receiptPrintingActive",
|
||||
"rejectAddressReuseActive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "notifications",
|
||||
"fields": [
|
||||
"sms",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
180
Vagrantfile
vendored
180
Vagrantfile
vendored
|
|
@ -1,180 +0,0 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||
# configures the configuration version (we support older styles for
|
||||
# backwards compatibility). Please don't change it unless you know what
|
||||
# you're doing.
|
||||
Vagrant.configure(2) do |config|
|
||||
# The most common configuration options are documented and commented below.
|
||||
# For a complete reference, please see the online documentation at
|
||||
# vagrantup.com
|
||||
|
||||
# Every Vagrant virtual environment requires a box to build off of.
|
||||
config.vm.box = "ubuntu/xenial64"
|
||||
|
||||
# Disable automatic box update checking. If you disable this, then
|
||||
# boxes will only be checked for updates when the user runs
|
||||
# `vagrant box outdated`. This is not recommended.
|
||||
# config.vm.box_check_update = false
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||
# config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||
|
||||
# Create a public network, which generally matched to bridged network.
|
||||
# Bridged networks make the machine appear as another physical device on
|
||||
# your network.
|
||||
# config.vm.network "public_network"
|
||||
|
||||
# If true, then any SSH connections made will enable agent forwarding.
|
||||
# Default value: false
|
||||
# config.ssh.forward_agent = true
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# the path on the host to the actual folder. The second argument is
|
||||
# the path on the guest to mount the folder. And the optional third
|
||||
# argument is a set of non-required options.
|
||||
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||
|
||||
# Provider-specific configuration so you can fine-tune various
|
||||
# backing providers for Vagrant. These expose provider-specific options.
|
||||
# Example for VirtualBox:
|
||||
#
|
||||
config.vm.provider "virtualbox" do |vb|
|
||||
# # Don't boot with headless mode
|
||||
# vb.gui = true
|
||||
#
|
||||
# # Use VBoxManage to customize the VM. For example to change memory:
|
||||
vb.customize ["modifyvm", :id, "--memory", "4096"]
|
||||
end
|
||||
#
|
||||
# View the documentation for the provider you're using for more
|
||||
# information on available options.
|
||||
|
||||
# Enable provisioning with CFEngine. CFEngine Community packages are
|
||||
# automatically installed. For example, configure the host as a
|
||||
# policy server and optionally a policy file to run:
|
||||
#
|
||||
# config.vm.provision "cfengine" do |cf|
|
||||
# cf.am_policy_hub = true
|
||||
# # cf.run_file = "motd.cf"
|
||||
# end
|
||||
#
|
||||
# You can also configure and bootstrap a client to an existing
|
||||
# policy server:
|
||||
#
|
||||
# config.vm.provision "cfengine" do |cf|
|
||||
# cf.policy_server_address = "10.0.2.15"
|
||||
# end
|
||||
|
||||
# Enable provisioning with Puppet stand alone. Puppet manifests
|
||||
# are contained in a directory path relative to this Vagrantfile.
|
||||
# You will need to create the manifests directory and a manifest in
|
||||
# the file default.pp in the manifests_path directory.
|
||||
#
|
||||
# config.vm.provision "puppet" do |puppet|
|
||||
# puppet.manifests_path = "manifests"
|
||||
# puppet.manifest_file = "default.pp"
|
||||
# end
|
||||
|
||||
# Enable provisioning with Chef Solo, specifying a cookbooks path, roles
|
||||
# path, and data_bags path (all relative to this Vagrantfile), and adding
|
||||
# some recipes and/or roles.
|
||||
#
|
||||
# config.vm.provision "chef_solo" do |chef|
|
||||
# chef.cookbooks_path = "~/chef/cookbooks"
|
||||
# chef.roles_path = "~/chef/roles"
|
||||
# chef.data_bags_path = "~/chef/data_bags"
|
||||
#
|
||||
# chef.add_recipe "mysql"
|
||||
# chef.add_role "web"
|
||||
#
|
||||
# chef.json = { mysql_password: "foo" }
|
||||
# end
|
||||
#
|
||||
# Chef Solo will automatically install the latest version of Chef for you.
|
||||
# This can be configured in the provisioner block:
|
||||
#
|
||||
# config.vm.provision "chef_solo" do |chef|
|
||||
# chef.version = "11.16.4"
|
||||
# end
|
||||
#
|
||||
# Alternative you can disable the installation of Chef entirely:
|
||||
#
|
||||
# config.vm.provision "chef_solo" do |chef|
|
||||
# chef.install = false
|
||||
# end
|
||||
|
||||
# Enable provisioning with Chef Zero. The Chef Zero provisioner accepts the
|
||||
# exact same parameter as the Chef Solo provisioner:
|
||||
#
|
||||
# config.vm.provision "chef_zero" do |chef|
|
||||
# chef.cookbooks_path = "~/chef/cookbooks"
|
||||
# chef.roles_path = "~/chef/roles"
|
||||
# chef.data_bags_path = "~/chef/data_bags"
|
||||
#
|
||||
# chef.add_recipe "mysql"
|
||||
# chef.add_role "web"
|
||||
#
|
||||
# # You may also specify custom JSON attributes:
|
||||
# chef.json = { mysql_password: "foo" }
|
||||
# end
|
||||
|
||||
# Enable provisioning with Chef Server, specifying the chef server URL,
|
||||
# and the path to the validation key (relative to this Vagrantfile).
|
||||
#
|
||||
# The Hosted Chef platform uses HTTPS. Substitute your organization for
|
||||
# ORGNAME in the URL and validation key.
|
||||
#
|
||||
# If you have your own Chef Server, use the appropriate URL, which may be
|
||||
# HTTP instead of HTTPS depending on your configuration. Also change the
|
||||
# validation key to validation.pem.
|
||||
#
|
||||
# config.vm.provision "chef_client" do |chef|
|
||||
# chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
|
||||
# chef.validation_key_path = "ORGNAME-validator.pem"
|
||||
# end
|
||||
#
|
||||
# If you're using the Hosted Chef platform, your validator client is
|
||||
# ORGNAME-validator, replacing ORGNAME with your organization name.
|
||||
#
|
||||
# If you have your own Chef Server, the default validation client name is
|
||||
# chef-validator, unless you changed the configuration.
|
||||
#
|
||||
# chef.validation_client_name = "ORGNAME-validator"
|
||||
#
|
||||
# Chef Client will automatically install the latest version of Chef for you.
|
||||
# This can be configured in the provisioner block:
|
||||
#
|
||||
# config.vm.provision "chef_client" do |chef|
|
||||
# chef.version = "11.16.4"
|
||||
# end
|
||||
#
|
||||
# Alternative you can disable the installation of Chef entirely:
|
||||
#
|
||||
# config.vm.provision "chef_client" do |chef|
|
||||
# chef.install = false
|
||||
# end
|
||||
|
||||
# Enable provisioning with Chef Apply, specifying an inline recipe to execute
|
||||
# on the target system.
|
||||
#
|
||||
# config.vm.provision "chef_apply" do |chef|
|
||||
# chef.recipe = <<-RECIPE
|
||||
# package "curl"
|
||||
# RECIPE
|
||||
# end
|
||||
#
|
||||
# Chef Apply will automatically install the latest version of Chef for you.
|
||||
# This can be configured in the provisioner block:
|
||||
#
|
||||
# config.vm.provision "chef_apply" do |chef|
|
||||
# chef.version = "11.16.4"
|
||||
# end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const adminServer = require('../lib/admin/admin-server')
|
||||
const adminServer = require('../lib/new-admin/admin-server')
|
||||
|
||||
adminServer.run()
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const applyDefaults = require('../lib/apply-defaults')
|
||||
|
||||
applyDefaults.run()
|
||||
.then(() => {
|
||||
console.log('Success.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -17,7 +17,6 @@ EOF
|
|||
|
||||
rm -f "/etc/lamassu/.migrate"
|
||||
lamassu-migrate
|
||||
lamassu-apply-defaults
|
||||
echo "Done."
|
||||
;;
|
||||
* )
|
||||
|
|
|
|||
|
|
@ -16,17 +16,15 @@ if (!name) {
|
|||
process.exit(2)
|
||||
}
|
||||
|
||||
login.generateOTP(name)
|
||||
.then(otp => {
|
||||
login.generateOTP(name).then(otp => {
|
||||
if (domain === 'localhost') {
|
||||
console.log(`https://${domain}:8070?otp=${otp}`)
|
||||
console.log(`https://${domain}:3000/register?otp=${otp}`)
|
||||
} else {
|
||||
console.log(`https://${domain}?otp=${otp}`)
|
||||
console.log(`https://${domain}/register?otp=${otp}`)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(err => {
|
||||
}).catch(err => {
|
||||
console.log('Error: %s', err)
|
||||
process.exit(3)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const adminServer = require('../lib/new-admin/admin-server')
|
||||
|
||||
adminServer.run()
|
||||
5
bin/old-lamassu-admin-server
Executable file
5
bin/old-lamassu-admin-server
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const adminServer = require('../lib/admin/admin-server')
|
||||
|
||||
adminServer.run()
|
||||
|
|
@ -16,15 +16,17 @@ if (!name) {
|
|||
process.exit(2)
|
||||
}
|
||||
|
||||
login.generateOTP(name).then(otp => {
|
||||
login.generateOTP(name)
|
||||
.then(otp => {
|
||||
if (domain === 'localhost') {
|
||||
console.log(`https://${domain}:3000/register?otp=${otp}`)
|
||||
console.log(`https://${domain}:8070?otp=${otp}`)
|
||||
} else {
|
||||
console.log(`https://${domain}/register?otp=${otp}`)
|
||||
console.log(`https://${domain}?otp=${otp}`)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}).catch(err => {
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Error: %s', err)
|
||||
process.exit(3)
|
||||
})
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const db = require('../lib/db')
|
||||
const configValidate = require('../lib/config-validate')
|
||||
|
||||
function pp (o) {
|
||||
console.log(require('util').inspect(o, {depth: null, colors: true}))
|
||||
}
|
||||
|
||||
function dbFetchConfig () {
|
||||
return db.oneOrNone(
|
||||
'select data from user_config where type=$1 order by created desc limit 1',
|
||||
['config']
|
||||
)
|
||||
.then(row => row && row.data)
|
||||
}
|
||||
|
||||
dbFetchConfig()
|
||||
.then(config => {
|
||||
pp(config)
|
||||
return configValidate.validate(config.config)
|
||||
})
|
||||
.then(() => {
|
||||
console.log('success.')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"cartridges": [1, 20],
|
||||
"virtualCartridges": [5]
|
||||
}
|
||||
35
certs.sh
35
certs.sh
|
|
@ -1,35 +0,0 @@
|
|||
DOMAIN=localhost
|
||||
|
||||
mkdir -p certs
|
||||
|
||||
openssl genrsa \
|
||||
-out certs/root-ca.key.pem \
|
||||
4096
|
||||
|
||||
openssl req \
|
||||
-x509 \
|
||||
-new \
|
||||
-nodes \
|
||||
-key certs/root-ca.key.pem \
|
||||
-days 3560 \
|
||||
-out certs/root-ca.crt.pem \
|
||||
-subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator CA/CN=lamassu-operator.is"
|
||||
|
||||
openssl genrsa \
|
||||
-out certs/server.key.pem \
|
||||
4096
|
||||
|
||||
# Create a request from your Device, which your Root CA will sign
|
||||
openssl req -new \
|
||||
-key certs/server.key.pem \
|
||||
-out certs/server.csr.pem \
|
||||
-subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator/CN=$DOMAIN"
|
||||
|
||||
# Sign the request from Device with your Root CA
|
||||
openssl x509 \
|
||||
-req -in certs/server.csr.pem \
|
||||
-CA certs/root-ca.crt.pem \
|
||||
-CAkey certs/root-ca.key.pem \
|
||||
-CAcreateserial \
|
||||
-out certs/server.crt.pem \
|
||||
-days 3650
|
||||
|
|
@ -1 +0,0 @@
|
|||
http://192.168.1.108:8070?otp=faa31556ce0d7c3f11315a7d58a3b009274087de4078bc55f07b58d784cc25a5
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"jsonSchema": {
|
||||
"title": "Crypto Settings",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"commission": {"type": "number", "title": "Cash-in Commission", "minimum": 0, "description": 3.5},
|
||||
"cashOutCommission": {"type": "number", "title": "Cash-out Commission", "minimum": 0},
|
||||
"fiatTxLimit": {"type": "number", "title": "Cash-out Limit", "minimum": 0}
|
||||
}
|
||||
},
|
||||
"uiSchema": {
|
||||
"ui:order": ["commission", "cashOutCommission", "fiatTxLimit"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
Countries: https://github.com/mledoze/countries
|
||||
8
lamassu-admin-elm/.gitignore
vendored
8
lamassu-admin-elm/.gitignore
vendored
|
|
@ -1,8 +0,0 @@
|
|||
node_modules
|
||||
elm-stuff
|
||||
|
||||
build/styles.css
|
||||
build/elm.js
|
||||
|
||||
.vscode
|
||||
.idea
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
# lamassu-admin
|
||||
|
||||
## Development
|
||||
|
||||
Start the hot-reloading webpack dev server:
|
||||
|
||||
npm start
|
||||
|
||||
Navigate to <http://localhost:8080>.
|
||||
|
||||
Any changes you make to your files (.elm, .js, .css, etc.) will trigger
|
||||
a hot reload.
|
||||
|
||||
## Production
|
||||
|
||||
When you're ready to deploy:
|
||||
|
||||
npm run build
|
||||
|
||||
This will create a `dist` folder:
|
||||
|
||||
.
|
||||
├── dist
|
||||
│ ├── index.html
|
||||
│ ├── 5df766af1ced8ff1fe0a.css
|
||||
│ └── 5df766af1ced8ff1fe0a.js
|
||||
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<base href="/">
|
||||
</head>
|
||||
<body>
|
||||
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
|
||||
<script src="bower_components/qrjs/qr.js"></script>
|
||||
<script src="bower_components/qr-code/src/qr-code.js"></script>
|
||||
<script src="elm.js"></script>
|
||||
<script>
|
||||
Elm.Main.fullscreen()
|
||||
</script>
|
||||
<link rel="stylesheet" href="bower_components/gridism/gridism.css">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
ls src/Css/* | entr elm-css src/Stylesheets.elm
|
||||
find src | entr elm-make src/Main.elm --output ../public/elm.js
|
||||
find src | entr elm-make src/Lamassu.elm --output ../public/lamassu-elm.js
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"summary": "helpful summary of your project, less than 80 characters",
|
||||
"repository": "https://github.com/user/project.git",
|
||||
"license": "Unlicense",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
"NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0",
|
||||
"arturopala/elm-monocle": "1.3.1 <= v < 2.0.0",
|
||||
"elm-community/json-extra": "2.0.0 <= v < 3.0.0",
|
||||
"elm-community/maybe-extra": "4.0.0 <= v < 5.0.0",
|
||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
||||
"elm-lang/http": "1.0.0 <= v < 2.0.0",
|
||||
"elm-lang/keyboard": "1.0.1 <= v < 2.0.0",
|
||||
"elm-lang/navigation": "2.0.1 <= v < 3.0.0",
|
||||
"elm-lang/virtual-dom": "2.0.0 <= v < 3.0.0",
|
||||
"elm-lang/websocket": "1.0.2 <= v < 2.0.0",
|
||||
"evancz/elm-markdown": "3.0.1 <= v < 4.0.0",
|
||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0",
|
||||
"ggb/numeral-elm": "1.2.3 <= v < 2.0.0",
|
||||
"justinmimbs/elm-date-extra": "2.0.2 <= v < 3.0.0",
|
||||
"krisajenkins/remotedata": "4.0.0 <= v < 5.0.0",
|
||||
"lukewestby/elm-http-builder": "5.1.0 <= v < 6.0.0",
|
||||
"pablohirafuji/elm-qrcode": "1.0.1 <= v < 2.0.0",
|
||||
"rluiten/stringdistance": "1.0.3 <= v < 2.0.0",
|
||||
"rtfeldman/elm-css": "10.0.0 <= v < 11.0.0",
|
||||
"rtfeldman/elm-css-helpers": "2.1.0 <= v < 3.0.0",
|
||||
"rtfeldman/elm-css-util": "1.0.2 <= v < 2.0.0",
|
||||
"rtfeldman/elm-validate": "1.1.3 <= v < 2.0.0",
|
||||
"tripokey/elm-fuzzy": "5.0.3 <= v < 6.0.0"
|
||||
},
|
||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
module Account exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Events exposing (..)
|
||||
import Html.Keyed
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import AccountTypes exposing (..)
|
||||
import AccountDecoder exposing (..)
|
||||
import AccountEncoder exposing (..)
|
||||
import FieldSet.Types
|
||||
import FieldSet.State
|
||||
import FieldSet.View
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes
|
||||
import Process
|
||||
import Time exposing (second)
|
||||
import Task
|
||||
|
||||
|
||||
type alias SubModel =
|
||||
{ status : SavingStatus
|
||||
, account : Account
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData SubModel
|
||||
|
||||
|
||||
type SavingStatus
|
||||
= Saving
|
||||
| Saved
|
||||
| Editing
|
||||
| NotSaving
|
||||
|
||||
|
||||
toModel : SavingStatus -> Account -> SubModel
|
||||
toModel status account =
|
||||
{ status = status, account = account }
|
||||
|
||||
|
||||
getForm : String -> Cmd Msg
|
||||
getForm code =
|
||||
get ("/api/account/" ++ code)
|
||||
|> withExpect (Http.expectJson accountDecoder)
|
||||
|> send (Result.map (toModel NotSaving) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
postForm : Account -> Cmd Msg
|
||||
postForm account =
|
||||
post "/api/account"
|
||||
|> withJsonBody (encodeAccount account)
|
||||
|> withExpect (Http.expectJson accountDecoder)
|
||||
|> send (Result.map (toModel Saved) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
RemoteData.NotAsked
|
||||
|
||||
|
||||
load : String -> ( Model, Cmd Msg )
|
||||
load code =
|
||||
( RemoteData.Loading, getForm code )
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
| Submit
|
||||
| FieldSetMsg FieldSet.Types.Msg
|
||||
| HideSaveIndication
|
||||
|
||||
|
||||
saveUpdate : SubModel -> ( SubModel, Cmd Msg )
|
||||
saveUpdate model =
|
||||
let
|
||||
cmd =
|
||||
if (model.status == Saved) then
|
||||
Process.sleep (2 * second)
|
||||
|> Task.perform (\_ -> HideSaveIndication)
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
model ! [ cmd ]
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg webModel =
|
||||
case msg of
|
||||
Load newModel ->
|
||||
RemoteData.update saveUpdate newModel
|
||||
|
||||
Submit ->
|
||||
RemoteData.update (\model -> model ! [ postForm model.account ]) webModel
|
||||
|
||||
HideSaveIndication ->
|
||||
RemoteData.update (\model -> { model | status = NotSaving } ! []) webModel
|
||||
|
||||
FieldSetMsg fieldSetMsg ->
|
||||
let
|
||||
updateFields model =
|
||||
FieldSet.State.update fieldSetMsg model.account.fields
|
||||
|
||||
newAccount account fields =
|
||||
{ account | fields = fields }
|
||||
|
||||
toModel model fieldsUpdate =
|
||||
{ model
|
||||
| account =
|
||||
newAccount model.account
|
||||
(Tuple.first fieldsUpdate)
|
||||
}
|
||||
! [ Cmd.map FieldSetMsg (Tuple.second fieldsUpdate) ]
|
||||
|
||||
mapper model =
|
||||
updateFields model
|
||||
|> (toModel model)
|
||||
in
|
||||
RemoteData.update mapper webModel
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view webModel =
|
||||
case webModel of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success model ->
|
||||
let
|
||||
fieldSetView =
|
||||
Html.Keyed.node "div" [] [ ( model.account.code, (Html.map FieldSetMsg (FieldSet.View.view model.account.fields)) ) ]
|
||||
|
||||
statusString =
|
||||
case model.status of
|
||||
Saved ->
|
||||
"Saved"
|
||||
|
||||
_ ->
|
||||
""
|
||||
in
|
||||
div []
|
||||
[ div [ class [ Css.Classes.SectionLabel ] ] [ text model.account.display ]
|
||||
, form [ id model.account.code ]
|
||||
[ fieldSetView
|
||||
, div [ class [ Css.Classes.ButtonRow ] ]
|
||||
[ div [ onClick Submit, class [ Css.Classes.Button ] ] [ text "Submit" ]
|
||||
, div [] [ text statusString ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
module AccountDecoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
import FieldSet.Rest exposing (..)
|
||||
import AccountTypes exposing (..)
|
||||
|
||||
|
||||
accountDecoder : Decoder Account
|
||||
accountDecoder =
|
||||
map3 Account
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
(field "fields" (list fieldDecoder))
|
||||
|
||||
|
||||
type alias AccountResult =
|
||||
Result String Account
|
||||
|
||||
|
||||
decodeAccount : String -> AccountResult
|
||||
decodeAccount string =
|
||||
decodeString accountDecoder string
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
module AccountEncoder exposing (..)
|
||||
|
||||
import Json.Encode exposing (..)
|
||||
import AccountTypes exposing (..)
|
||||
import List
|
||||
import FieldSet.Rest exposing (..)
|
||||
|
||||
|
||||
encodeAccount : Account -> Value
|
||||
encodeAccount account =
|
||||
Json.Encode.object
|
||||
[ ( "code", string account.code )
|
||||
, ( "display", string account.display )
|
||||
, ( "fields", list (List.filterMap encodeField account.fields) )
|
||||
]
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
module AccountTypes exposing (..)
|
||||
|
||||
import FieldSet.Types exposing (..)
|
||||
|
||||
|
||||
type alias Account =
|
||||
{ code : String
|
||||
, display : String
|
||||
, fields : List Field
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
module AccountsDecoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
|
||||
|
||||
accountDecoder : Decoder ( String, String )
|
||||
accountDecoder =
|
||||
map2 (,)
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
|
||||
|
||||
accountsDecoder : Decoder (List ( String, String ))
|
||||
accountsDecoder =
|
||||
map identity
|
||||
(field "accounts" (list accountDecoder))
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
module BasicTypes exposing (..)
|
||||
|
||||
|
||||
type SavingStatus
|
||||
= Saving
|
||||
| Saved
|
||||
| Editing
|
||||
| NotSaving
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
module ClientServerWebsocket exposing (..)
|
||||
|
||||
-- Elm might not be the best platform for this kind of thing
|
||||
-- Hard to do a global lookup table
|
||||
-- Might be easiest to just use HTTP for this for now
|
||||
-- No need to prematurely optimize and go against the flow
|
||||
|
||||
import WebSocket
|
||||
|
||||
|
||||
type Msg
|
||||
= OK
|
||||
| Timeout
|
||||
|
||||
|
||||
type alias Client =
|
||||
Sub Msg
|
||||
|
||||
|
||||
init : String -> Client
|
||||
init url =
|
||||
let
|
||||
sub =
|
||||
WebSocket.listen url parsePacket
|
||||
in
|
||||
sub
|
||||
|
||||
|
||||
request : Client -> String -> String -> Cmd Msg
|
||||
request client url payload =
|
||||
let
|
||||
cmd =
|
||||
Cmd.map (respond client) (WebSocket.send url payload)
|
||||
in
|
||||
cmd
|
||||
|
||||
|
||||
parsePacket : String -> Msg
|
||||
parsePacket packet =
|
||||
OK
|
||||
|
||||
|
||||
respond : client -> (a -> Msg)
|
||||
respond client =
|
||||
(\_ -> OK)
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
module Common.Customer.Decoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Extra exposing (date, fromResult)
|
||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
||||
import Common.Customer.Types exposing (..)
|
||||
|
||||
|
||||
customersDecoder : Decoder (List Customer)
|
||||
customersDecoder =
|
||||
field "customers" (list customerDecoder)
|
||||
|
||||
|
||||
mapAuthorizedTypes : String -> Decoder Authorized
|
||||
mapAuthorizedTypes s =
|
||||
case s of
|
||||
"blocked" ->
|
||||
succeed Blocked
|
||||
|
||||
"verified" ->
|
||||
succeed Verified
|
||||
|
||||
"automatic" ->
|
||||
succeed Automatic
|
||||
|
||||
_ ->
|
||||
fail ("No such type " ++ s)
|
||||
|
||||
|
||||
authorizedDecoder : Decoder Authorized
|
||||
authorizedDecoder =
|
||||
string
|
||||
|> andThen mapAuthorizedTypes
|
||||
|
||||
|
||||
idCardDataDecoder : Decoder IdCardData
|
||||
idCardDataDecoder =
|
||||
decode IdCardData
|
||||
|> required "uid" string
|
||||
|
||||
customerDecoder : Decoder Customer
|
||||
customerDecoder =
|
||||
decode Customer
|
||||
|> required "id" string
|
||||
|> required "name" (nullable string)
|
||||
|> required "phone" (nullable string)
|
||||
|> required "phoneAt" (nullable date)
|
||||
|> required "smsOverride" authorizedDecoder
|
||||
|> required "smsOverrideByName" (nullable string)
|
||||
|> required "smsOverrideAt" (nullable date)
|
||||
|> required "created" date
|
||||
|> required "status" (nullable string)
|
||||
|> required "authorizedOverride" authorizedDecoder
|
||||
|> required "authorizedOverrideByName" (nullable string)
|
||||
|> required "authorizedOverrideAt" (nullable date)
|
||||
|> required "authorizedAt" (nullable date)
|
||||
|> required "idCardData" (nullable idCardDataDecoder)
|
||||
|> required "idCardDataOverride" authorizedDecoder
|
||||
|> required "idCardDataOverrideByName" (nullable string)
|
||||
|> required "idCardDataOverrideAt" (nullable date)
|
||||
|> required "idCardDataAt" (nullable date)
|
||||
|> required "idCardPhotoPath" (nullable string)
|
||||
|> required "idCardPhotoOverride" authorizedDecoder
|
||||
|> required "idCardPhotoOverrideByName" (nullable string)
|
||||
|> required "idCardPhotoOverrideAt" (nullable date)
|
||||
|> required "idCardPhotoAt" (nullable date)
|
||||
|> required "sanctions" (nullable bool)
|
||||
|> required "sanctionsOverride" authorizedDecoder
|
||||
|> required "sanctionsOverrideByName" (nullable string)
|
||||
|> required "sanctionsOverrideAt" (nullable date)
|
||||
|> required "sanctionsAt" (nullable date)
|
||||
|> required "frontCameraPath" (nullable string)
|
||||
|> required "frontCameraOverride" authorizedDecoder
|
||||
|> required "frontCameraOverrideByName" (nullable string)
|
||||
|> required "frontCameraOverrideAt" (nullable date)
|
||||
|> required "frontCameraAt" (nullable date)
|
||||
|> required "dailyVolume" (nullable string)
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
module Common.Customer.Types exposing (..)
|
||||
|
||||
import Date exposing (Date)
|
||||
|
||||
|
||||
type Authorized
|
||||
= Automatic
|
||||
| Blocked
|
||||
| Verified
|
||||
|
||||
|
||||
type alias Customers =
|
||||
List Customer
|
||||
|
||||
|
||||
type alias IdCardData =
|
||||
{ uid : String }
|
||||
|
||||
type alias Customer =
|
||||
{ id : String
|
||||
, name : Maybe String
|
||||
, phone : Maybe String
|
||||
, phoneAt : Maybe Date
|
||||
, smsOverride : Authorized
|
||||
, smsOverrideByName : Maybe String
|
||||
, smsOverrideAt : Maybe Date
|
||||
, created : Date
|
||||
, status : Maybe String
|
||||
, authorizedOverride : Authorized
|
||||
, authorizedOverrideByName : Maybe String
|
||||
, authorizedOverrideAt : Maybe Date
|
||||
, authorizedAt : Maybe Date
|
||||
, idCardData : Maybe IdCardData
|
||||
, idCardDataOverride : Authorized
|
||||
, idCardDataOverrideByName : Maybe String
|
||||
, idCardDataOverrideAt : Maybe Date
|
||||
, idCardDataAt : Maybe Date
|
||||
, idCardPhotoPath : Maybe String
|
||||
, idCardPhotoOverride : Authorized
|
||||
, idCardPhotoOverrideByName : Maybe String
|
||||
, idCardPhotoOverrideAt : Maybe Date
|
||||
, idCardPhotoAt : Maybe Date
|
||||
, sanctions : Maybe Bool
|
||||
, sanctionsOverride : Authorized
|
||||
, sanctionsOverrideByName : Maybe String
|
||||
, sanctionsOverrideAt : Maybe Date
|
||||
, sanctionsAt : Maybe Date
|
||||
, frontCameraPath : Maybe String
|
||||
, frontCameraOverride : Authorized
|
||||
, frontCameraOverrideByName : Maybe String
|
||||
, frontCameraOverrideAt : Maybe Date
|
||||
, frontCameraAt : Maybe Date
|
||||
, dailyVolume : Maybe String
|
||||
}
|
||||
|
||||
|
||||
authorizedToString : Authorized -> String
|
||||
authorizedToString model =
|
||||
case model of
|
||||
Verified ->
|
||||
"verified"
|
||||
|
||||
Blocked ->
|
||||
"blocked"
|
||||
|
||||
Automatic ->
|
||||
"automatic"
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
module Common.Logs.Decoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Extra exposing (date, fromResult)
|
||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
||||
import Common.Logs.Types exposing (..)
|
||||
|
||||
|
||||
logsDecoder : Decoder Logs
|
||||
logsDecoder =
|
||||
decode Logs
|
||||
|> required "logs" (list logDecoder)
|
||||
|> required "currentMachine" machineDecoder
|
||||
|
||||
|
||||
logDecoder : Decoder Log
|
||||
logDecoder =
|
||||
decode Log
|
||||
|> required "id" string
|
||||
|> required "timestamp" date
|
||||
|> required "logLevel" string
|
||||
|> required "message" string
|
||||
|
||||
|
||||
supportLogsDecoder : Decoder SupportLogs
|
||||
supportLogsDecoder =
|
||||
field "supportLogs" (list supportLogDecoder)
|
||||
|
||||
|
||||
latestLogSnapshotDecoder : Decoder SupportLogSnapshot
|
||||
latestLogSnapshotDecoder =
|
||||
decode SupportLogSnapshot
|
||||
|> required "deviceId" string
|
||||
|> required "timestamp" date
|
||||
|
||||
|
||||
supportLogDecoder : Decoder SupportLog
|
||||
supportLogDecoder =
|
||||
decode SupportLog
|
||||
|> required "id" string
|
||||
|> required "deviceId" string
|
||||
|> required "timestamp" date
|
||||
|> required "name" string
|
||||
|
||||
|
||||
machinesDecoder : Decoder Machines
|
||||
machinesDecoder =
|
||||
field "machines" (list machineDecoder)
|
||||
|
||||
|
||||
machineDecoder : Decoder Machine
|
||||
machineDecoder =
|
||||
decode Machine
|
||||
|> required "deviceId" string
|
||||
|> required "name" string
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
module Common.Logs.Types exposing (..)
|
||||
|
||||
import Date exposing (Date)
|
||||
|
||||
|
||||
type alias Machine =
|
||||
{ deviceId : String
|
||||
, name : String
|
||||
}
|
||||
|
||||
|
||||
type alias Machines =
|
||||
List Machine
|
||||
|
||||
|
||||
type alias Log =
|
||||
{ id : String
|
||||
, timestamp : Date
|
||||
, logLevel : String
|
||||
, message : String
|
||||
}
|
||||
|
||||
|
||||
type alias SupportLogSnapshot =
|
||||
{ deviceId : String
|
||||
, timestamp : Date
|
||||
}
|
||||
|
||||
|
||||
type alias SupportLog =
|
||||
{ id : String
|
||||
, deviceId : String
|
||||
, timestamp : Date
|
||||
, name : String
|
||||
}
|
||||
|
||||
|
||||
type alias SupportLogs =
|
||||
List SupportLog
|
||||
|
||||
|
||||
type alias Logs =
|
||||
{ logs : List Log, currentMachine : Machine }
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
module Common.TransactionTypes exposing (..)
|
||||
|
||||
import Date exposing (Date)
|
||||
|
||||
type CryptoCode
|
||||
= BTC
|
||||
| BCH
|
||||
| ETH
|
||||
| ZEC
|
||||
| DASH
|
||||
| LTC
|
||||
|
||||
|
||||
type alias CashInTxRec =
|
||||
{ id : String
|
||||
, machineName : String
|
||||
, toAddress : String
|
||||
, cryptoAtoms : Int
|
||||
, cryptoCode : CryptoCode
|
||||
, fiat : Float
|
||||
, commissionPercentage : Maybe Float
|
||||
, rawTickerPrice : Maybe Float
|
||||
, fiatCode : String
|
||||
, txHash : Maybe String
|
||||
, phone : Maybe String
|
||||
, error : Maybe String
|
||||
, operatorCompleted : Bool
|
||||
, send : Bool
|
||||
, sendConfirmed : Bool
|
||||
, expired : Bool
|
||||
, created : Date
|
||||
}
|
||||
|
||||
|
||||
type alias CashOutTxRec =
|
||||
{ id : String
|
||||
, machineName : String
|
||||
, toAddress : String
|
||||
, cryptoAtoms : Int
|
||||
, cryptoCode : CryptoCode
|
||||
, fiat : Float
|
||||
, commissionPercentage : Maybe Float
|
||||
, rawTickerPrice : Maybe Float
|
||||
, fiatCode : String
|
||||
, status : String
|
||||
, dispense : Bool
|
||||
, notified : Bool
|
||||
, redeemed : Bool
|
||||
, phone : Maybe String
|
||||
, error : Maybe String
|
||||
, created : Date
|
||||
, confirmed : Bool
|
||||
, expired : Bool
|
||||
}
|
||||
|
||||
|
||||
type Tx
|
||||
= CashInTx CashInTxRec
|
||||
| CashOutTx CashOutTxRec
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,305 +0,0 @@
|
|||
module ConfigDecoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
import ConfigTypes exposing (..)
|
||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded, custom)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
fieldValueTypeDecoder : String -> Decoder FieldValue
|
||||
fieldValueTypeDecoder fieldType =
|
||||
case fieldType of
|
||||
"string" ->
|
||||
map FieldStringValue (field "value" string)
|
||||
|
||||
"percentage" ->
|
||||
map FieldPercentageValue (field "value" float)
|
||||
|
||||
"integer" ->
|
||||
map FieldIntegerValue (field "value" int)
|
||||
|
||||
"decimal" ->
|
||||
map FieldDecimalValue (field "value" float)
|
||||
|
||||
"onOff" ->
|
||||
map FieldOnOffValue (field "value" bool)
|
||||
|
||||
"account" ->
|
||||
map FieldFiatCurrencyValue (field "value" string)
|
||||
|
||||
"fiatCurrency" ->
|
||||
map FieldFiatCurrencyValue (field "value" string)
|
||||
|
||||
"cryptoCurrency" ->
|
||||
map FieldCryptoCurrencyValue (field "value" (list string))
|
||||
|
||||
"language" ->
|
||||
map FieldLanguageValue (field "value" (list string))
|
||||
|
||||
"country" ->
|
||||
map FieldCountryValue (field "value" string)
|
||||
|
||||
"textarea" ->
|
||||
map FieldTextAreaValue (field "value" string)
|
||||
|
||||
"markdown" ->
|
||||
map FieldMarkdownValue (field "value" string)
|
||||
|
||||
_ ->
|
||||
fail ("Unsupported field type: " ++ fieldType)
|
||||
|
||||
|
||||
fieldValueDecoder : Decoder FieldValue
|
||||
fieldValueDecoder =
|
||||
(field "fieldType" string) |> andThen fieldValueTypeDecoder
|
||||
|
||||
|
||||
fieldScopeDecoder : Decoder FieldScope
|
||||
fieldScopeDecoder =
|
||||
map2 FieldScope
|
||||
(field "crypto" cryptoDecoder)
|
||||
(field "machine" machineDecoder)
|
||||
|
||||
|
||||
nullOr : Decoder a -> Decoder (Maybe a)
|
||||
nullOr decoder =
|
||||
oneOf
|
||||
[ null Nothing
|
||||
, map Just decoder
|
||||
]
|
||||
|
||||
|
||||
fieldLocatorDecoder : Decoder FieldLocator
|
||||
fieldLocatorDecoder =
|
||||
map4 FieldLocator
|
||||
(field "fieldScope" fieldScopeDecoder)
|
||||
(field "code" string)
|
||||
((field "fieldType" string) |> andThen fieldTypeDecoder)
|
||||
(field "fieldClass" (nullOr string))
|
||||
|
||||
|
||||
fieldDecoder : Decoder Field
|
||||
fieldDecoder =
|
||||
map5 Field
|
||||
(field "fieldLocator" fieldLocatorDecoder)
|
||||
(field "fieldValue" fieldValueDecoder)
|
||||
(field "fieldEnabledIfAny" (list string))
|
||||
(field "fieldEnabledIfAll" (list string))
|
||||
(succeed True)
|
||||
|
||||
|
||||
string2machine : String -> Machine
|
||||
string2machine s =
|
||||
if s == "global" then
|
||||
GlobalMachine
|
||||
else
|
||||
MachineId s
|
||||
|
||||
|
||||
machineDecoder : Decoder Machine
|
||||
machineDecoder =
|
||||
map string2machine string
|
||||
|
||||
|
||||
cryptoDecoder : Decoder Crypto
|
||||
cryptoDecoder =
|
||||
map stringToCrypto string
|
||||
|
||||
|
||||
displayRecDecoder : Decoder DisplayRec
|
||||
displayRecDecoder =
|
||||
map2 DisplayRec
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
|
||||
|
||||
machineDisplayDecoder : Decoder MachineDisplay
|
||||
machineDisplayDecoder =
|
||||
map2 MachineDisplay
|
||||
(field "machine" machineDecoder)
|
||||
(field "display" string)
|
||||
|
||||
|
||||
cryptoDisplayDecoder : Decoder CryptoDisplay
|
||||
cryptoDisplayDecoder =
|
||||
map2 CryptoDisplay
|
||||
(field "crypto" cryptoDecoder)
|
||||
(field "display" string)
|
||||
|
||||
|
||||
stringToConfigScope : String -> Decoder ConfigScope
|
||||
stringToConfigScope s =
|
||||
case s of
|
||||
"global" ->
|
||||
succeed Global
|
||||
|
||||
"specific" ->
|
||||
succeed Specific
|
||||
|
||||
"both" ->
|
||||
succeed Both
|
||||
|
||||
_ ->
|
||||
fail ("No such ConfigScope " ++ s)
|
||||
|
||||
|
||||
basicFieldTypeDecoder : String -> Decoder FieldType
|
||||
basicFieldTypeDecoder s =
|
||||
case s of
|
||||
"string" ->
|
||||
succeed FieldStringType
|
||||
|
||||
"percentage" ->
|
||||
succeed FieldPercentageType
|
||||
|
||||
"integer" ->
|
||||
succeed FieldIntegerType
|
||||
|
||||
"decimal" ->
|
||||
succeed FieldDecimalType
|
||||
|
||||
"onOff" ->
|
||||
succeed FieldOnOffType
|
||||
|
||||
"account" ->
|
||||
succeed FieldAccountType
|
||||
|
||||
"fiatCurrency" ->
|
||||
succeed FieldFiatCurrencyType
|
||||
|
||||
"cryptoCurrency" ->
|
||||
succeed FieldCryptoCurrencyType
|
||||
|
||||
"language" ->
|
||||
succeed FieldLanguageType
|
||||
|
||||
"country" ->
|
||||
succeed FieldCountryType
|
||||
|
||||
"textarea" ->
|
||||
succeed FieldTextAreaType
|
||||
|
||||
"markdown" ->
|
||||
succeed FieldMarkdownType
|
||||
|
||||
_ ->
|
||||
fail ("No such FieldType " ++ s)
|
||||
|
||||
|
||||
configScopeDecoder : Decoder ConfigScope
|
||||
configScopeDecoder =
|
||||
string
|
||||
|> andThen stringToConfigScope
|
||||
|
||||
|
||||
fieldTypeDecoder : String -> Decoder FieldType
|
||||
fieldTypeDecoder fieldType =
|
||||
basicFieldTypeDecoder fieldType
|
||||
|
||||
|
||||
fieldValidatorDecode : String -> Decoder FieldValidator
|
||||
fieldValidatorDecode code =
|
||||
case code of
|
||||
"min" ->
|
||||
map FieldMin (field "min" int)
|
||||
|
||||
"max" ->
|
||||
map FieldMax (field "max" int)
|
||||
|
||||
"required" ->
|
||||
succeed FieldRequired
|
||||
|
||||
_ ->
|
||||
fail ("Unsupported fieldValidator: " ++ code)
|
||||
|
||||
|
||||
fieldValidatorDecoder : Decoder FieldValidator
|
||||
fieldValidatorDecoder =
|
||||
(field "code" string)
|
||||
|> andThen fieldValidatorDecode
|
||||
|
||||
|
||||
displayTopDecoderHelper : Maybe Int -> Decoder DisplayTop
|
||||
displayTopDecoderHelper maybeDisplayTopCount =
|
||||
case maybeDisplayTopCount of
|
||||
Nothing ->
|
||||
(maybe <| (field "displayTop" string))
|
||||
|> map (DisplayTopSolo << (Maybe.withDefault ""))
|
||||
|
||||
Just 0 ->
|
||||
succeed DisplayTopNone
|
||||
|
||||
Just 1 ->
|
||||
succeed DisplayTopNone
|
||||
|
||||
Just x ->
|
||||
map (DisplayTopLeader x) (field "displayTop" string)
|
||||
|
||||
|
||||
displayTopDecoder : Decoder DisplayTop
|
||||
displayTopDecoder =
|
||||
(maybe <| (field "displayTopCount" int))
|
||||
|> andThen displayTopDecoderHelper
|
||||
|
||||
|
||||
fieldDescriptorDecoder : Decoder FieldDescriptor
|
||||
fieldDescriptorDecoder =
|
||||
decode FieldDescriptor
|
||||
|> required "code" string
|
||||
|> required "cryptoScope" configScopeDecoder
|
||||
|> required "machineScope" configScopeDecoder
|
||||
|> custom displayTopDecoder
|
||||
|> required "displayBottom" string
|
||||
|> custom (maybe (field "displayCount" int))
|
||||
|> custom (field "fieldType" string |> andThen fieldTypeDecoder)
|
||||
|> custom (field "fieldValidation" <| list fieldValidatorDecoder)
|
||||
|> required "fieldClass" (nullable string)
|
||||
|> required "fieldEnabledIfAny" (list string)
|
||||
|> required "fieldEnabledIfAll" (list string)
|
||||
|> optional "readOnly" bool False
|
||||
|
||||
|
||||
configSchemaDecoder : Decoder ConfigSchema
|
||||
configSchemaDecoder =
|
||||
map5 ConfigSchema
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
(field "cryptoScope" configScopeDecoder)
|
||||
(field "machineScope" configScopeDecoder)
|
||||
(field "entries" (list fieldDescriptorDecoder))
|
||||
|
||||
|
||||
configGroupDecoder : Decoder ConfigGroup
|
||||
configGroupDecoder =
|
||||
map4 ConfigGroup
|
||||
(field "schema" configSchemaDecoder)
|
||||
(field "values" (list fieldDecoder))
|
||||
(field "selectedCryptos" (list string))
|
||||
(field "data" configDataDecoder)
|
||||
|
||||
|
||||
accountRecDecoder : Decoder AccountRec
|
||||
accountRecDecoder =
|
||||
oneOf
|
||||
[ map4 AccountRec
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
(field "class" string)
|
||||
(field "cryptos" (map Just (list cryptoDecoder)))
|
||||
, map4 AccountRec
|
||||
(field "code" string)
|
||||
(field "display" string)
|
||||
(field "class" string)
|
||||
(succeed Nothing)
|
||||
]
|
||||
|
||||
|
||||
configDataDecoder : Decoder ConfigData
|
||||
configDataDecoder =
|
||||
map6 ConfigData
|
||||
(field "cryptoCurrencies" (list cryptoDisplayDecoder))
|
||||
(field "currencies" (list displayRecDecoder))
|
||||
(field "languages" (list displayRecDecoder))
|
||||
(field "countries" (list displayRecDecoder))
|
||||
(field "accounts" (list accountRecDecoder))
|
||||
(field "machines" (list machineDisplayDecoder))
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
module ConfigEncoder exposing (..)
|
||||
|
||||
import Json.Encode exposing (..)
|
||||
import List
|
||||
import ConfigTypes exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
encodeFieldValueObject : String -> Value -> Value
|
||||
encodeFieldValueObject fieldTypeStr value =
|
||||
object [ ( "fieldType", string fieldTypeStr ), ( "value", value ) ]
|
||||
|
||||
|
||||
encodeFieldValue : FieldValue -> Value
|
||||
encodeFieldValue fieldValue =
|
||||
case fieldValue of
|
||||
FieldStringValue value ->
|
||||
encodeFieldValueObject "string" (string value)
|
||||
|
||||
FieldPercentageValue value ->
|
||||
encodeFieldValueObject "percentage" (float value)
|
||||
|
||||
FieldIntegerValue value ->
|
||||
encodeFieldValueObject "integer" (int value)
|
||||
|
||||
FieldDecimalValue value ->
|
||||
encodeFieldValueObject "decimal" (float value)
|
||||
|
||||
FieldOnOffValue value ->
|
||||
encodeFieldValueObject "onOff" (bool value)
|
||||
|
||||
FieldAccountValue value ->
|
||||
encodeFieldValueObject "account" (string value)
|
||||
|
||||
FieldFiatCurrencyValue value ->
|
||||
encodeFieldValueObject "fiatCurrency" (string value)
|
||||
|
||||
FieldCryptoCurrencyValue value ->
|
||||
encodeFieldValueObject "cryptoCurrency" (list (List.map string value))
|
||||
|
||||
FieldLanguageValue value ->
|
||||
encodeFieldValueObject "language" (list (List.map string value))
|
||||
|
||||
FieldCountryValue value ->
|
||||
encodeFieldValueObject "country" (string value)
|
||||
|
||||
FieldTextAreaValue value ->
|
||||
encodeFieldValueObject "textarea" (string value)
|
||||
|
||||
FieldMarkdownValue value ->
|
||||
encodeFieldValueObject "markdown" (string value)
|
||||
|
||||
|
||||
encodeCrypto : Crypto -> Value
|
||||
encodeCrypto crypto =
|
||||
case crypto of
|
||||
CryptoCode cryptoCode ->
|
||||
string cryptoCode
|
||||
|
||||
GlobalCrypto ->
|
||||
string "global"
|
||||
|
||||
|
||||
encodeMachine : Machine -> Value
|
||||
encodeMachine machine =
|
||||
case machine of
|
||||
MachineId machineId ->
|
||||
string machineId
|
||||
|
||||
GlobalMachine ->
|
||||
string "global"
|
||||
|
||||
|
||||
encodeFieldScope : FieldScope -> Value
|
||||
encodeFieldScope fieldScope =
|
||||
Json.Encode.object
|
||||
[ ( "crypto", encodeCrypto fieldScope.crypto )
|
||||
, ( "machine", encodeMachine fieldScope.machine )
|
||||
]
|
||||
|
||||
|
||||
fieldTypeEncoder : FieldType -> Value
|
||||
fieldTypeEncoder fieldType =
|
||||
case fieldType of
|
||||
FieldStringType ->
|
||||
string "string"
|
||||
|
||||
FieldPercentageType ->
|
||||
string "percentage"
|
||||
|
||||
FieldIntegerType ->
|
||||
string "integer"
|
||||
|
||||
FieldDecimalType ->
|
||||
string "decimal"
|
||||
|
||||
FieldOnOffType ->
|
||||
string "onOff"
|
||||
|
||||
FieldAccountType ->
|
||||
string "account"
|
||||
|
||||
FieldFiatCurrencyType ->
|
||||
string "fiatCurrency"
|
||||
|
||||
FieldCryptoCurrencyType ->
|
||||
string "cryptoCurrency"
|
||||
|
||||
FieldLanguageType ->
|
||||
string "language"
|
||||
|
||||
FieldCountryType ->
|
||||
string "country"
|
||||
|
||||
FieldTextAreaType ->
|
||||
string "textarea"
|
||||
|
||||
FieldMarkdownType ->
|
||||
string "markdown"
|
||||
|
||||
|
||||
maybeString : Maybe String -> Value
|
||||
maybeString maybeString =
|
||||
case maybeString of
|
||||
Nothing ->
|
||||
null
|
||||
|
||||
Just s ->
|
||||
string s
|
||||
|
||||
|
||||
encodeFieldLocator : FieldLocator -> Value
|
||||
encodeFieldLocator fieldLocator =
|
||||
Json.Encode.object
|
||||
[ ( "fieldScope", encodeFieldScope fieldLocator.fieldScope )
|
||||
, ( "code", string fieldLocator.code )
|
||||
, ( "fieldType", fieldTypeEncoder fieldLocator.fieldType )
|
||||
, ( "fieldClass", maybeString fieldLocator.fieldClass )
|
||||
]
|
||||
|
||||
|
||||
encodeFieldResult : FieldInstance -> Maybe Value
|
||||
encodeFieldResult fieldInstance =
|
||||
let
|
||||
encode value =
|
||||
Json.Encode.object
|
||||
[ ( "fieldLocator", encodeFieldLocator fieldInstance.fieldLocator )
|
||||
, ( "fieldValue", value )
|
||||
]
|
||||
|
||||
dirtyEncode fieldHolder =
|
||||
case fieldHolder of
|
||||
ParsingError fieldValue ->
|
||||
Nothing
|
||||
|
||||
ValidationError fieldValue ->
|
||||
Nothing
|
||||
|
||||
FieldOk fieldValue ->
|
||||
if (fieldInstance.loadedFieldHolder == fieldHolder) then
|
||||
Nothing
|
||||
else
|
||||
Just <| encode <| encodeFieldValue fieldValue
|
||||
|
||||
FieldEmpty ->
|
||||
if (fieldInstance.loadedFieldHolder == fieldHolder) then
|
||||
Nothing
|
||||
else
|
||||
Just <| encode null
|
||||
in
|
||||
dirtyEncode fieldInstance.fieldHolder
|
||||
|
||||
|
||||
encodeResults : String -> List FieldInstance -> Maybe Value
|
||||
encodeResults configGroupCode fieldInstances =
|
||||
let
|
||||
results =
|
||||
List.filterMap encodeFieldResult fieldInstances
|
||||
in
|
||||
if List.isEmpty results then
|
||||
Nothing
|
||||
else
|
||||
Json.Encode.object
|
||||
[ ( "groupCode", string configGroupCode )
|
||||
, ( "values", list (List.filterMap encodeFieldResult fieldInstances) )
|
||||
]
|
||||
|> Just
|
||||
|
|
@ -1,495 +0,0 @@
|
|||
module ConfigTypes exposing (..)
|
||||
|
||||
import String
|
||||
import Selectize
|
||||
|
||||
|
||||
type alias DisplayRec =
|
||||
{ code : String
|
||||
, display : String
|
||||
}
|
||||
|
||||
|
||||
type Machine
|
||||
= MachineId String
|
||||
| GlobalMachine
|
||||
|
||||
|
||||
type alias MachineDisplay =
|
||||
{ machine : Machine
|
||||
, display : String
|
||||
}
|
||||
|
||||
|
||||
type ConfigScope
|
||||
= Global
|
||||
| Specific
|
||||
| Both
|
||||
|
||||
|
||||
type FieldHolder
|
||||
= ParsingError String
|
||||
| ValidationError String
|
||||
| FieldOk FieldValue
|
||||
| FieldEmpty
|
||||
|
||||
|
||||
type alias FieldScope =
|
||||
{ crypto : Crypto
|
||||
, machine : Machine
|
||||
}
|
||||
|
||||
|
||||
type alias FieldLocator =
|
||||
{ fieldScope : FieldScope
|
||||
, code : String
|
||||
, fieldType : FieldType
|
||||
, fieldClass : Maybe String
|
||||
}
|
||||
|
||||
|
||||
type FieldComponent
|
||||
= InputBoxComponent
|
||||
| TextAreaComponent
|
||||
| SelectizeComponent Selectize.State
|
||||
|
||||
|
||||
type alias FieldInstance =
|
||||
{ fieldLocator : FieldLocator
|
||||
, component : FieldComponent
|
||||
, fieldHolder : FieldHolder
|
||||
, loadedFieldHolder : FieldHolder
|
||||
, fieldValidation : List FieldValidator
|
||||
, fieldEnabledIfAny : List String
|
||||
, fieldEnabledIfAll : List String
|
||||
, readOnly : Bool
|
||||
, inScope : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias ResolvedFieldInstance =
|
||||
{ fieldLocator : FieldLocator
|
||||
, fieldValue : Maybe FieldValue
|
||||
}
|
||||
|
||||
|
||||
type alias Field =
|
||||
{ fieldLocator : FieldLocator
|
||||
, fieldValue : FieldValue
|
||||
, fieldEnabledIfAny : List String
|
||||
, fieldEnabledIfAll : List String
|
||||
, inScope : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias FieldMeta =
|
||||
{ fieldLocator : FieldLocator
|
||||
, fieldEnabledIfAny : List String
|
||||
, fieldEnabledIfAll : List String
|
||||
, inScope : Bool
|
||||
}
|
||||
|
||||
|
||||
type FieldType
|
||||
= FieldStringType
|
||||
| FieldPercentageType
|
||||
| FieldIntegerType
|
||||
| FieldDecimalType
|
||||
| FieldOnOffType
|
||||
| FieldAccountType
|
||||
| FieldFiatCurrencyType
|
||||
| FieldCryptoCurrencyType
|
||||
| FieldLanguageType
|
||||
| FieldCountryType
|
||||
| FieldTextAreaType
|
||||
| FieldMarkdownType
|
||||
|
||||
|
||||
type FieldValue
|
||||
= FieldStringValue String
|
||||
| FieldPercentageValue Float
|
||||
| FieldIntegerValue Int
|
||||
| FieldDecimalValue Float
|
||||
| FieldOnOffValue Bool
|
||||
| FieldAccountValue String
|
||||
| FieldFiatCurrencyValue String
|
||||
| FieldCryptoCurrencyValue (List String)
|
||||
| FieldLanguageValue (List String)
|
||||
| FieldCountryValue String
|
||||
| FieldTextAreaValue String
|
||||
| FieldMarkdownValue String
|
||||
|
||||
|
||||
type FieldValidator
|
||||
= FieldMin Int
|
||||
| FieldMax Int
|
||||
| FieldRequired
|
||||
|
||||
|
||||
type DisplayTop
|
||||
= DisplayTopLeader Int String
|
||||
| DisplayTopSolo String
|
||||
| DisplayTopNone
|
||||
|
||||
|
||||
type alias FieldDescriptor =
|
||||
{ code : String
|
||||
, cryptoScope : ConfigScope
|
||||
, machineScope : ConfigScope
|
||||
, displayTop : DisplayTop
|
||||
, displayBottom : String
|
||||
, displayCount : Maybe Int
|
||||
, fieldType : FieldType
|
||||
, fieldValidation : List FieldValidator
|
||||
, fieldClass : Maybe String
|
||||
, fieldEnabledIfAny : List String
|
||||
, fieldEnabledIfAll : List String
|
||||
, readOnly : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias ConfigSchema =
|
||||
{ code : String
|
||||
, display : String
|
||||
, cryptoScope : ConfigScope
|
||||
, machineScope : ConfigScope
|
||||
, entries : List FieldDescriptor
|
||||
}
|
||||
|
||||
|
||||
type alias ConfigGroup =
|
||||
{ schema : ConfigSchema
|
||||
, values : List Field
|
||||
, selectedCryptos : List String
|
||||
, data : ConfigData
|
||||
}
|
||||
|
||||
|
||||
type alias AccountRec =
|
||||
{ code : String
|
||||
, display : String
|
||||
, class : String
|
||||
, cryptos : Maybe (List Crypto)
|
||||
}
|
||||
|
||||
|
||||
accountRecToDisplayRec : AccountRec -> DisplayRec
|
||||
accountRecToDisplayRec accountRec =
|
||||
{ code = accountRec.code
|
||||
, display = accountRec.display
|
||||
}
|
||||
|
||||
|
||||
type alias ConfigData =
|
||||
{ cryptoCurrencies : List CryptoDisplay
|
||||
, currencies : List DisplayRec
|
||||
, languages : List DisplayRec
|
||||
, countries : List DisplayRec
|
||||
, accounts : List AccountRec
|
||||
, machines : List MachineDisplay
|
||||
}
|
||||
|
||||
|
||||
type alias FieldCollection =
|
||||
{ fields : List Field
|
||||
, fieldInstances : List FieldInstance
|
||||
}
|
||||
|
||||
|
||||
initFieldCollection : FieldCollection
|
||||
initFieldCollection =
|
||||
{ fields = []
|
||||
, fieldInstances = []
|
||||
}
|
||||
|
||||
|
||||
globalCryptoDisplay : CryptoDisplay
|
||||
globalCryptoDisplay =
|
||||
{ crypto = GlobalCrypto
|
||||
, display = "Global"
|
||||
}
|
||||
|
||||
|
||||
globalMachineDisplay : MachineDisplay
|
||||
globalMachineDisplay =
|
||||
{ machine = GlobalMachine
|
||||
, display = "Global"
|
||||
}
|
||||
|
||||
|
||||
fieldValueToDisplay : FieldValue -> String
|
||||
fieldValueToDisplay fieldValue =
|
||||
case fieldValue of
|
||||
FieldOnOffValue v ->
|
||||
if v then
|
||||
"On"
|
||||
else
|
||||
"Off"
|
||||
|
||||
_ ->
|
||||
fieldValueToString fieldValue
|
||||
|
||||
|
||||
fieldValueToString : FieldValue -> String
|
||||
fieldValueToString fieldValue =
|
||||
case fieldValue of
|
||||
FieldStringValue v ->
|
||||
v
|
||||
|
||||
FieldPercentageValue v ->
|
||||
toString v
|
||||
|
||||
FieldIntegerValue v ->
|
||||
toString v
|
||||
|
||||
FieldDecimalValue v ->
|
||||
toString v
|
||||
|
||||
FieldOnOffValue v ->
|
||||
if v then
|
||||
"on"
|
||||
else
|
||||
"off"
|
||||
|
||||
FieldAccountValue v ->
|
||||
v
|
||||
|
||||
FieldFiatCurrencyValue v ->
|
||||
v
|
||||
|
||||
FieldCryptoCurrencyValue v ->
|
||||
String.join "," v
|
||||
|
||||
FieldLanguageValue v ->
|
||||
String.join "," v
|
||||
|
||||
FieldCountryValue v ->
|
||||
v
|
||||
|
||||
FieldTextAreaValue v ->
|
||||
v
|
||||
|
||||
FieldMarkdownValue v ->
|
||||
v
|
||||
|
||||
|
||||
machineToString : Machine -> String
|
||||
machineToString machine =
|
||||
case machine of
|
||||
GlobalMachine ->
|
||||
"global"
|
||||
|
||||
MachineId machineId ->
|
||||
machineId
|
||||
|
||||
|
||||
listMachines : ConfigGroup -> List MachineDisplay
|
||||
listMachines configGroup =
|
||||
case configGroup.schema.machineScope of
|
||||
Specific ->
|
||||
configGroup.data.machines
|
||||
|
||||
Global ->
|
||||
[ globalMachineDisplay ]
|
||||
|
||||
Both ->
|
||||
globalMachineDisplay :: configGroup.data.machines
|
||||
|
||||
|
||||
isCrypto : String -> CryptoDisplay -> Bool
|
||||
isCrypto cryptoString cryptoDisplay =
|
||||
case cryptoDisplay.crypto of
|
||||
GlobalCrypto ->
|
||||
cryptoString == "global"
|
||||
|
||||
CryptoCode string ->
|
||||
cryptoString == string
|
||||
|
||||
|
||||
lookupCryptoDisplay : List CryptoDisplay -> String -> Maybe CryptoDisplay
|
||||
lookupCryptoDisplay cryptoDisplays cryptoString =
|
||||
List.filter (isCrypto cryptoString) cryptoDisplays
|
||||
|> List.head
|
||||
|
||||
|
||||
fieldHolderToCryptoStrings : FieldHolder -> List String
|
||||
fieldHolderToCryptoStrings fieldHolder =
|
||||
case fieldHolder of
|
||||
FieldOk fieldValue ->
|
||||
case fieldValue of
|
||||
FieldCryptoCurrencyValue cryptoStrings ->
|
||||
cryptoStrings
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
|
||||
|
||||
allCryptos : List CryptoDisplay -> ConfigScope -> List String -> List CryptoDisplay
|
||||
allCryptos cryptoDisplays cryptoScope cryptoStrings =
|
||||
let
|
||||
allSpecificCryptos =
|
||||
List.filterMap (lookupCryptoDisplay cryptoDisplays) cryptoStrings
|
||||
in
|
||||
case cryptoScope of
|
||||
Global ->
|
||||
[ globalCryptoDisplay ]
|
||||
|
||||
Specific ->
|
||||
allSpecificCryptos
|
||||
|
||||
Both ->
|
||||
globalCryptoDisplay :: allSpecificCryptos
|
||||
|
||||
|
||||
listCryptos : ConfigGroup -> List CryptoDisplay
|
||||
listCryptos configGroup =
|
||||
case configGroup.schema.cryptoScope of
|
||||
Specific ->
|
||||
configGroup.data.cryptoCurrencies
|
||||
|
||||
Global ->
|
||||
[ globalCryptoDisplay ]
|
||||
|
||||
Both ->
|
||||
globalCryptoDisplay :: configGroup.data.cryptoCurrencies
|
||||
|
||||
|
||||
fieldScopes : ConfigGroup -> List FieldScope
|
||||
fieldScopes configGroup =
|
||||
let
|
||||
machines =
|
||||
List.map .machine (listMachines configGroup)
|
||||
|
||||
cryptos =
|
||||
List.map .crypto (listCryptos configGroup)
|
||||
|
||||
cryptoScopes crypto =
|
||||
List.map (\machine -> { machine = machine, crypto = crypto }) machines
|
||||
in
|
||||
List.concatMap cryptoScopes cryptos
|
||||
|
||||
|
||||
stringToCrypto : String -> Crypto
|
||||
stringToCrypto string =
|
||||
case string of
|
||||
"global" ->
|
||||
GlobalCrypto
|
||||
|
||||
_ ->
|
||||
CryptoCode string
|
||||
|
||||
|
||||
fieldHolderToMaybe : FieldHolder -> Maybe FieldValue
|
||||
fieldHolderToMaybe fieldHolder =
|
||||
case fieldHolder of
|
||||
FieldOk fieldValue ->
|
||||
Just fieldValue
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
||||
resultToFieldHolder : Result String FieldValue -> FieldHolder
|
||||
resultToFieldHolder result =
|
||||
case result of
|
||||
Ok fieldValue ->
|
||||
FieldOk fieldValue
|
||||
|
||||
Err s ->
|
||||
ParsingError s
|
||||
|
||||
|
||||
stringToFieldHolder : FieldType -> String -> FieldHolder
|
||||
stringToFieldHolder fieldType s =
|
||||
if (String.isEmpty s) then
|
||||
FieldEmpty
|
||||
else
|
||||
case fieldType of
|
||||
FieldStringType ->
|
||||
FieldOk (FieldStringValue s)
|
||||
|
||||
FieldPercentageType ->
|
||||
String.toFloat s
|
||||
|> Result.map FieldPercentageValue
|
||||
|> resultToFieldHolder
|
||||
|
||||
FieldIntegerType ->
|
||||
String.toInt s
|
||||
|> Result.map FieldIntegerValue
|
||||
|> resultToFieldHolder
|
||||
|
||||
FieldDecimalType ->
|
||||
String.toFloat s
|
||||
|> Result.map FieldDecimalValue
|
||||
|> resultToFieldHolder
|
||||
|
||||
FieldOnOffType ->
|
||||
case s of
|
||||
"on" ->
|
||||
FieldOk (FieldOnOffValue True)
|
||||
|
||||
"off" ->
|
||||
FieldOk (FieldOnOffValue False)
|
||||
|
||||
_ ->
|
||||
ParsingError ("Unsupported value for OnOff: " ++ s)
|
||||
|
||||
FieldAccountType ->
|
||||
FieldOk (FieldAccountValue s)
|
||||
|
||||
FieldFiatCurrencyType ->
|
||||
FieldOk (FieldFiatCurrencyValue s)
|
||||
|
||||
FieldCryptoCurrencyType ->
|
||||
FieldOk (FieldCryptoCurrencyValue [ s ])
|
||||
|
||||
FieldLanguageType ->
|
||||
FieldOk (FieldLanguageValue [ s ])
|
||||
|
||||
FieldCountryType ->
|
||||
FieldOk (FieldCountryValue s)
|
||||
|
||||
FieldTextAreaType ->
|
||||
FieldOk (FieldTextAreaValue s)
|
||||
|
||||
FieldMarkdownType ->
|
||||
FieldOk (FieldMarkdownValue s)
|
||||
|
||||
|
||||
groupMember : ConfigGroup -> String -> Bool
|
||||
groupMember configGroup fieldCode =
|
||||
List.any (.code >> ((==) fieldCode)) configGroup.schema.entries
|
||||
|
||||
|
||||
fieldHolderMap : a -> (FieldValue -> a) -> FieldHolder -> a
|
||||
fieldHolderMap default mapper fieldHolder =
|
||||
case fieldHolder of
|
||||
FieldOk v ->
|
||||
mapper v
|
||||
|
||||
_ ->
|
||||
default
|
||||
|
||||
|
||||
type Crypto
|
||||
= CryptoCode String
|
||||
| GlobalCrypto
|
||||
|
||||
|
||||
type alias CryptoDisplay =
|
||||
{ crypto : Crypto
|
||||
, display : String
|
||||
}
|
||||
|
||||
|
||||
cryptoToString : Crypto -> String
|
||||
cryptoToString crypto =
|
||||
case crypto of
|
||||
GlobalCrypto ->
|
||||
"global"
|
||||
|
||||
CryptoCode code ->
|
||||
code
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
module CoreTypes
|
||||
exposing
|
||||
( Msg(..)
|
||||
, Category(..)
|
||||
, Route(..)
|
||||
)
|
||||
|
||||
import Navigation
|
||||
import Pair
|
||||
import Account
|
||||
import Config
|
||||
import MaintenanceMachines.Types
|
||||
import MaintenanceFunding.Types
|
||||
import Transaction.Types
|
||||
import Transactions
|
||||
import Customers.Types
|
||||
import Customer.Types
|
||||
import Logs.Types
|
||||
import SupportLogs.Types
|
||||
import StatusTypes
|
||||
|
||||
|
||||
type Category
|
||||
= AccountCat
|
||||
| MachineSettingsCat
|
||||
| GlobalSettingsCat
|
||||
| MaintenanceCat
|
||||
|
||||
|
||||
type Route
|
||||
= AccountRoute String
|
||||
| PairRoute
|
||||
| ConfigRoute String (Maybe String)
|
||||
| TransactionsRoute
|
||||
| TransactionRoute String
|
||||
| CustomersRoute
|
||||
| CustomerRoute String
|
||||
| LogsRoute (Maybe String)
|
||||
| SupportLogsRoute (Maybe String)
|
||||
| MaintenanceMachinesRoute
|
||||
| MaintenanceFundingRoute (Maybe String)
|
||||
| NotFoundRoute
|
||||
|
||||
|
||||
type Msg
|
||||
= AccountMsg Account.Msg
|
||||
| PairMsg Pair.Msg
|
||||
| ConfigMsg Config.Msg
|
||||
| MaintenanceMachinesMsg MaintenanceMachines.Types.Msg
|
||||
| MaintenanceFundingMsg MaintenanceFunding.Types.Msg
|
||||
| TransactionsMsg Transactions.Msg
|
||||
| TransactionMsg Transaction.Types.Msg
|
||||
| CustomersMsg Customers.Types.Msg
|
||||
| CustomerMsg Customer.Types.Msg
|
||||
| LogsMsg Logs.Types.Msg
|
||||
| SupportLogsMsg SupportLogs.Types.Msg
|
||||
| LoadAccounts (List ( String, String ))
|
||||
| LoadStatus StatusTypes.WebStatus
|
||||
| NewUrl String
|
||||
| UrlChange Navigation.Location
|
||||
| Interval
|
||||
| WebSocketMsg String
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
module Css.Admin exposing (className, class, classList, id)
|
||||
|
||||
import Css.Helpers
|
||||
import Html
|
||||
import Html.CssHelpers
|
||||
|
||||
|
||||
name : String
|
||||
name =
|
||||
"lamassuAdmin"
|
||||
|
||||
|
||||
helpers : Html.CssHelpers.Namespace String class id msg
|
||||
helpers =
|
||||
Html.CssHelpers.withNamespace name
|
||||
|
||||
|
||||
className : class -> String
|
||||
className class =
|
||||
Css.Helpers.identifierToString name class
|
||||
|
||||
|
||||
class : List class -> Html.Attribute msg
|
||||
class =
|
||||
helpers.class
|
||||
|
||||
|
||||
classList : List ( class, Bool ) -> Html.Attribute msg
|
||||
classList =
|
||||
helpers.classList
|
||||
|
||||
|
||||
id : id -> Html.Attribute msg
|
||||
id =
|
||||
helpers.id
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
module Css.Classes exposing (..)
|
||||
|
||||
|
||||
|
||||
type CssClasses
|
||||
= Layout
|
||||
| Main
|
||||
| PaneWrapper
|
||||
| LeftPane
|
||||
| ContentPane
|
||||
| NavBar
|
||||
| MainLeft
|
||||
| MainRight
|
||||
| NavBarItemActive
|
||||
| NavBarCategoryContainer
|
||||
| NavBarCategory
|
||||
| NavBarRoute
|
||||
| Container
|
||||
| Content
|
||||
| CryptoTabs
|
||||
| CryptoTab
|
||||
| CryptoTabsActive
|
||||
| SectionLabel
|
||||
| ConfigTable
|
||||
| ConfigTableGlobalRow
|
||||
| ConfigContainer
|
||||
| TopDisplay
|
||||
| BottomDisplay
|
||||
| MultiDisplay
|
||||
| ShortCell
|
||||
| MediumCell
|
||||
| LongCell
|
||||
| TextCell
|
||||
| FormRow
|
||||
| Button
|
||||
| ButtonRow
|
||||
| Active
|
||||
| BasicInput
|
||||
| BasicInputDisabled
|
||||
| BasicInputReadOnly
|
||||
| CellDisabled
|
||||
| NoInput
|
||||
| Component
|
||||
| FocusedComponent
|
||||
| InvalidComponent
|
||||
| TableButton
|
||||
| Fail
|
||||
| Success
|
||||
| StatusBar
|
||||
| InvalidGroup
|
||||
| NumberColumn
|
||||
| DirectionColumn
|
||||
| TruncatedColumn
|
||||
| DateColumn
|
||||
| TxTable
|
||||
| InputContainer
|
||||
| UnitDisplay
|
||||
| EmptyTable
|
||||
| Saving
|
||||
| Enabled
|
||||
| Disabled
|
||||
| TxId
|
||||
| TxDate
|
||||
| TxMachine
|
||||
| TxAmount
|
||||
| TxFiat
|
||||
| TxCrypto
|
||||
| TxPhone
|
||||
| TxAddress
|
||||
| TxCancelled
|
||||
| QrCode
|
||||
| CashOut
|
||||
| CashIn
|
||||
| ReadOnly
|
||||
| CryptoAddress
|
||||
| BalanceSection
|
||||
| Textarea
|
||||
| SelectizeAccount
|
||||
| SelectizeFiatCurrency
|
||||
| SelectizeCryptoCurrency
|
||||
| SelectizeLanguage
|
||||
| SelectizeCountry
|
||||
| SelectizeOnOff
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
module Css.ColorSchemes exposing (..)
|
||||
|
||||
import Css exposing (..)
|
||||
import Css.LocalColors as Colors
|
||||
import Css.Classes exposing (..)
|
||||
|
||||
|
||||
type alias ColorScheme =
|
||||
{ bg : Color
|
||||
, fg : Color
|
||||
, bgHover : Color
|
||||
, fgActive : Color
|
||||
, bgActive : Color
|
||||
}
|
||||
|
||||
|
||||
darkGreyScheme : ColorScheme
|
||||
darkGreyScheme =
|
||||
{ bg = Colors.darkGrey
|
||||
, fg = Colors.sandstone
|
||||
, bgHover = Colors.darkerGrey
|
||||
, fgActive = Colors.amazonite
|
||||
, bgActive = Colors.darkerGrey
|
||||
}
|
||||
|
||||
|
||||
darkerGreyScheme : ColorScheme
|
||||
darkerGreyScheme =
|
||||
{ bg = Colors.darkerGrey
|
||||
, fg = Colors.sandstone
|
||||
, bgHover = Colors.darkerGrey
|
||||
, fgActive = Colors.amazonite
|
||||
, bgActive = Colors.darkerGrey
|
||||
}
|
||||
|
||||
|
||||
lightGreyScheme : ColorScheme
|
||||
lightGreyScheme =
|
||||
{ bg = Colors.darkerLightGrey
|
||||
, fg = Colors.sandstone
|
||||
, bgHover = Colors.lighterLightGrey
|
||||
, fgActive = Colors.sandstone
|
||||
, bgActive = Colors.lightGrey
|
||||
}
|
||||
|
||||
|
||||
cobaltScheme : ColorScheme
|
||||
cobaltScheme =
|
||||
{ bg = Colors.cobalt
|
||||
, fg = Colors.white
|
||||
, bgHover = Colors.darkCobalt
|
||||
, fgActive = Colors.amazonite
|
||||
, bgActive = Colors.darkCobalt
|
||||
}
|
||||
|
||||
|
||||
colorize : ColorScheme -> Style
|
||||
colorize scheme =
|
||||
batch
|
||||
[ color scheme.fg
|
||||
, fontWeight bold
|
||||
, cursor pointer
|
||||
, backgroundColor scheme.bg
|
||||
, hover
|
||||
[ backgroundColor scheme.bgHover
|
||||
]
|
||||
, active [ color scheme.fgActive ]
|
||||
, withClass Active
|
||||
[ color scheme.fgActive
|
||||
, backgroundColor scheme.bgActive
|
||||
]
|
||||
]
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
module Css.LocalColors exposing (..)
|
||||
|
||||
import Css exposing (..)
|
||||
|
||||
|
||||
cobalt : Color
|
||||
cobalt =
|
||||
hex "004062"
|
||||
|
||||
|
||||
darkCobalt : Color
|
||||
darkCobalt =
|
||||
hex "042c47"
|
||||
|
||||
|
||||
amazonite : Color
|
||||
amazonite =
|
||||
hex "37e8d7"
|
||||
|
||||
|
||||
white : Color
|
||||
white =
|
||||
hex "ffffff"
|
||||
|
||||
|
||||
sandstone : Color
|
||||
sandstone =
|
||||
hex "5f5f56"
|
||||
|
||||
|
||||
lightGrey : Color
|
||||
lightGrey =
|
||||
hex "f6f6f4"
|
||||
|
||||
|
||||
lighterLightGrey : Color
|
||||
lighterLightGrey =
|
||||
hex "fcfcfa"
|
||||
|
||||
|
||||
darkerLightGrey : Color
|
||||
darkerLightGrey =
|
||||
hex "E6E6E3"
|
||||
|
||||
|
||||
darkGrey : Color
|
||||
darkGrey =
|
||||
hex "2d2d2d"
|
||||
|
||||
|
||||
darkerGrey : Color
|
||||
darkerGrey =
|
||||
hex "282828"
|
||||
|
||||
|
||||
red : Color
|
||||
red =
|
||||
hex "eb6b6e"
|
||||
|
||||
|
||||
lightRed : Color
|
||||
lightRed =
|
||||
hex "efd1d2"
|
||||
|
||||
|
||||
disabledGrey : Color
|
||||
disabledGrey =
|
||||
hex "757575"
|
||||
|
|
@ -1,523 +0,0 @@
|
|||
module Css.Main exposing (..)
|
||||
|
||||
import Css exposing (..)
|
||||
import Css.Elements
|
||||
exposing
|
||||
( body
|
||||
, li
|
||||
, a
|
||||
, div
|
||||
, td
|
||||
, th
|
||||
, tr
|
||||
, thead
|
||||
, tbody
|
||||
, input
|
||||
, button
|
||||
, label
|
||||
, p
|
||||
, svg
|
||||
, h2
|
||||
)
|
||||
import Css.Namespace exposing (namespace)
|
||||
import Css.LocalColors as Colors
|
||||
import Css.ColorSchemes exposing (..)
|
||||
import Css.Classes exposing (..)
|
||||
import Css.Selectize
|
||||
|
||||
|
||||
type CssIds
|
||||
= Page
|
||||
|
||||
|
||||
mainBackgroundColor : Color
|
||||
mainBackgroundColor =
|
||||
Colors.lightGrey
|
||||
|
||||
|
||||
contentBackgroundColor : Color
|
||||
contentBackgroundColor =
|
||||
Colors.white
|
||||
|
||||
|
||||
navBackgroundColor : Color
|
||||
navBackgroundColor =
|
||||
Colors.darkGrey
|
||||
|
||||
|
||||
navItemActiveBackgroundColor : Color
|
||||
navItemActiveBackgroundColor =
|
||||
Colors.darkerGrey
|
||||
|
||||
|
||||
navItemActiveColor : Color
|
||||
navItemActiveColor =
|
||||
Colors.amazonite
|
||||
|
||||
|
||||
navItemColor : Color
|
||||
navItemColor =
|
||||
Colors.sandstone
|
||||
|
||||
|
||||
cryptoTabsBackgroundColor : Color
|
||||
cryptoTabsBackgroundColor =
|
||||
Colors.cobalt
|
||||
|
||||
|
||||
cryptoTabsHoverBackgroundColor : Color
|
||||
cryptoTabsHoverBackgroundColor =
|
||||
Colors.darkCobalt
|
||||
|
||||
|
||||
cryptoTabsColor : Color
|
||||
cryptoTabsColor =
|
||||
Colors.white
|
||||
|
||||
|
||||
cryptoTabsActiveColor : Color
|
||||
cryptoTabsActiveColor =
|
||||
Colors.amazonite
|
||||
|
||||
|
||||
cobaltBG : Color
|
||||
cobaltBG =
|
||||
Colors.cobalt
|
||||
|
||||
|
||||
cobaltHoverBG : Color
|
||||
cobaltHoverBG =
|
||||
Colors.darkCobalt
|
||||
|
||||
|
||||
cobaltColor : Color
|
||||
cobaltColor =
|
||||
Colors.white
|
||||
|
||||
|
||||
cobaltActiveColor : Color
|
||||
cobaltActiveColor =
|
||||
Colors.amazonite
|
||||
|
||||
|
||||
codeFonts : List String
|
||||
codeFonts =
|
||||
[ "Inconsolata", "monospace" ]
|
||||
|
||||
|
||||
css : Stylesheet
|
||||
css =
|
||||
(stylesheet << namespace "lamassuAdmin")
|
||||
[ body
|
||||
[ fontFamilies [ "Nunito", "sans-serif" ]
|
||||
, margin zero
|
||||
]
|
||||
, p
|
||||
[ margin zero ]
|
||||
, class QrCode
|
||||
[ backgroundColor Colors.lightGrey
|
||||
, padding (px 10)
|
||||
, marginBottom (px 20)
|
||||
, borderRadius (px 6)
|
||||
, descendants
|
||||
[ svg
|
||||
[ height (px 400)
|
||||
, width (px 400)
|
||||
]
|
||||
]
|
||||
]
|
||||
, class Layout
|
||||
[]
|
||||
, class Main
|
||||
[ displayFlex
|
||||
, marginBottom (px 40)
|
||||
]
|
||||
, class PaneWrapper
|
||||
[ displayFlex
|
||||
]
|
||||
, class LeftPane
|
||||
[ minWidth (px 270)
|
||||
]
|
||||
, class ContentPane
|
||||
[ maxHeight (pct 100)
|
||||
]
|
||||
, class StatusBar
|
||||
[ position fixed
|
||||
, bottom zero
|
||||
, padding2 (px 10) (px 20)
|
||||
, backgroundColor Colors.sandstone
|
||||
, color Colors.white
|
||||
, width (pct 100)
|
||||
]
|
||||
, class CashOut
|
||||
[ backgroundColor Colors.lightGrey
|
||||
]
|
||||
, class FormRow
|
||||
[ margin2 (px 20) zero
|
||||
, firstChild
|
||||
[ margin zero
|
||||
]
|
||||
, descendants
|
||||
[ label
|
||||
[ fontSize (px 11)
|
||||
, fontWeight bold
|
||||
, children
|
||||
[ div
|
||||
[ margin3 zero zero (px 5)
|
||||
, color Colors.sandstone
|
||||
]
|
||||
]
|
||||
]
|
||||
, input
|
||||
[ border zero
|
||||
, backgroundColor Colors.white
|
||||
, borderRadius (px 3)
|
||||
, padding (px 6)
|
||||
, textAlign left
|
||||
, fontFamilies codeFonts
|
||||
, fontSize (px 14)
|
||||
, fontWeight (int 600)
|
||||
, width (pct 90)
|
||||
, property "outline" "none"
|
||||
]
|
||||
]
|
||||
]
|
||||
, class ButtonRow
|
||||
[ textAlign right ]
|
||||
, class Button
|
||||
[ colorize cobaltScheme
|
||||
, padding2 (px 10) (px 15)
|
||||
, display inlineBlock
|
||||
, borderRadius (px 5)
|
||||
, withClass Disabled
|
||||
[ backgroundColor Colors.darkerLightGrey
|
||||
, color Colors.white
|
||||
, cursor default
|
||||
]
|
||||
]
|
||||
, class MainLeft
|
||||
[ backgroundColor navBackgroundColor
|
||||
, height (pct 100)
|
||||
]
|
||||
, class MainRight
|
||||
[ backgroundColor mainBackgroundColor
|
||||
, height (pct 100)
|
||||
]
|
||||
, class Content
|
||||
[ margin (px 20)
|
||||
, backgroundColor contentBackgroundColor
|
||||
, borderRadius (px 5)
|
||||
]
|
||||
, class Container
|
||||
[ padding (px 30)
|
||||
, backgroundColor Colors.lightGrey
|
||||
, borderRadius4 (px 0) (px 5) (px 5) (px 5)
|
||||
, width (em 30)
|
||||
]
|
||||
, class CryptoAddress
|
||||
[ fontFamilies codeFonts ]
|
||||
, class BalanceSection
|
||||
[ marginTop (em 2)
|
||||
, descendants
|
||||
[ h2
|
||||
[ fontSize (em 1.2)
|
||||
, marginBottom (em 0.2)
|
||||
]
|
||||
]
|
||||
]
|
||||
, class Textarea
|
||||
[ width (pct 100)
|
||||
, border (px 0)
|
||||
, backgroundColor transparent
|
||||
]
|
||||
, class CryptoTabs
|
||||
[ displayFlex
|
||||
, children
|
||||
[ class CryptoTab
|
||||
[ padding2 (px 10) (px 15)
|
||||
, colorize lightGreyScheme
|
||||
, textDecoration none
|
||||
, firstChild
|
||||
[ borderRadius4 (px 5) (px 0) (px 0) (px 0)
|
||||
]
|
||||
, lastChild
|
||||
[ borderRadius4 (px 0) (px 5) (px 0) (px 0)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, class SectionLabel
|
||||
[ fontWeight bold
|
||||
, fontSize (px 30)
|
||||
, marginBottom (px 10)
|
||||
]
|
||||
, class ConfigContainer
|
||||
[ padding2 (px 20) (px 60)
|
||||
, borderRadius4 (px 0) (px 7) (px 7) (px 7)
|
||||
, backgroundColor mainBackgroundColor
|
||||
, margin3 zero zero (px 10)
|
||||
, property "animation" "fadein 0.8s"
|
||||
, overflow hidden
|
||||
, minHeight (em 15)
|
||||
, minWidth (em 20)
|
||||
]
|
||||
, class NoInput
|
||||
[ fontFamilies codeFonts
|
||||
, color Colors.sandstone
|
||||
, fontWeight normal
|
||||
, textAlign left |> important
|
||||
]
|
||||
, class TxTable
|
||||
[ borderRadius (px 7)
|
||||
, margin2 (px 20) zero
|
||||
, property "border-collapse" "collapse"
|
||||
, fontSize (px 14)
|
||||
, width (pct 100)
|
||||
, backgroundColor Colors.white
|
||||
, descendants
|
||||
[ a
|
||||
[ textDecoration none
|
||||
, color Colors.sandstone
|
||||
, borderBottom3 (px 1) solid Colors.amazonite
|
||||
]
|
||||
, class NumberColumn
|
||||
[ textAlign right
|
||||
, width (em 10)
|
||||
]
|
||||
, class DirectionColumn
|
||||
[ textAlign left
|
||||
, fontWeight bold
|
||||
, fontSize (pct 90)
|
||||
]
|
||||
, class TxCancelled
|
||||
[ backgroundColor Colors.lightRed ]
|
||||
, tbody
|
||||
[ fontFamilies codeFonts
|
||||
, color Colors.sandstone
|
||||
, descendants
|
||||
[ td
|
||||
[ padding2 (px 2) (px 14)
|
||||
, borderBottom3 (px 1) solid Colors.lightGrey
|
||||
, whiteSpace noWrap
|
||||
]
|
||||
, class TruncatedColumn
|
||||
[ maxWidth zero
|
||||
, overflow hidden
|
||||
, width (px 300)
|
||||
, textOverflow ellipsis
|
||||
]
|
||||
, class TxDate [ width (em 10) ]
|
||||
, class TxAddress
|
||||
[ width (em 25)
|
||||
]
|
||||
]
|
||||
]
|
||||
, thead
|
||||
[ fontSize (px 14)
|
||||
, textAlign center
|
||||
, color Colors.sandstone
|
||||
, descendants
|
||||
[ td
|
||||
[ borderBottom3 (px 2) solid Colors.lightGrey
|
||||
, padding (px 5)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, class EmptyTable
|
||||
[ fontSize (px 20)
|
||||
, fontWeight normal
|
||||
]
|
||||
, class ConfigTable
|
||||
[ fontSize (px 14)
|
||||
, fontWeight bold
|
||||
, borderRadius (px 7)
|
||||
, margin2 (px 20) zero
|
||||
, property "border-collapse" "collapse"
|
||||
, descendants
|
||||
[ class Css.Selectize.SelectizeContainer
|
||||
[ Css.Selectize.component
|
||||
, border3 (px 2) solid Colors.darkerLightGrey
|
||||
, borderRadius (px 3)
|
||||
]
|
||||
, class InputContainer
|
||||
[ displayFlex
|
||||
, property "justify-content" "flex-end"
|
||||
, border3 (px 2) solid Colors.darkerLightGrey
|
||||
, borderRadius (px 3)
|
||||
]
|
||||
, class UnitDisplay
|
||||
[ backgroundColor Colors.darkerLightGrey
|
||||
, color Colors.sandstone
|
||||
, padding2 zero (px 5)
|
||||
, fontWeight (int 700)
|
||||
, fontSize (pct 80)
|
||||
, lineHeight (px 25)
|
||||
, cursor default
|
||||
, fontFamilies [ "Nunito", "sans-serif" ]
|
||||
]
|
||||
, input
|
||||
[ border zero
|
||||
, borderRadius (px 3)
|
||||
, padding (px 6)
|
||||
, textAlign right
|
||||
, width (pct 100)
|
||||
, fontFamilies codeFonts
|
||||
, fontWeight (int 600)
|
||||
, fontSize (px 14)
|
||||
, outline none
|
||||
, backgroundColor Colors.white
|
||||
]
|
||||
, class CellDisabled
|
||||
[ property "background" "repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px)"
|
||||
]
|
||||
, class BasicInput
|
||||
[ pseudoElement "placeholder"
|
||||
[ color Colors.amazonite
|
||||
, opacity (num 1)
|
||||
]
|
||||
]
|
||||
, class BasicInputDisabled
|
||||
[ height (px 25)
|
||||
, lineHeight (px 25)
|
||||
, fontSize (px 14)
|
||||
, fontWeight (int 500)
|
||||
, color Colors.sandstone
|
||||
, opacity (num 0.7)
|
||||
, textAlign left
|
||||
, padding2 zero (em 1)
|
||||
, property "background" "repeating-linear-gradient(45deg,#dfdfdc,#dfdfdc 2px,#e6e6e3 5px)"
|
||||
]
|
||||
, class ReadOnly
|
||||
[ lineHeight (px 25)
|
||||
, backgroundColor Colors.lightGrey
|
||||
, fontFamilies codeFonts
|
||||
, fontSize (px 14)
|
||||
, fontWeight (int 600)
|
||||
, color Colors.sandstone
|
||||
, cursor default
|
||||
, children
|
||||
[ class BasicInputReadOnly
|
||||
[ padding2 zero (px 5)
|
||||
]
|
||||
]
|
||||
]
|
||||
, td
|
||||
[ padding2 (px 3) (px 4)
|
||||
, textAlign center
|
||||
, verticalAlign middle
|
||||
, width (em 5)
|
||||
]
|
||||
, class Component
|
||||
[ borderRadius (px 3)
|
||||
, border3 (px 2) solid Colors.lightGrey
|
||||
, backgroundColor Colors.white
|
||||
]
|
||||
, class FocusedComponent
|
||||
[ children
|
||||
[ class InputContainer
|
||||
[ borderColor Colors.amazonite ]
|
||||
]
|
||||
]
|
||||
, class InvalidComponent
|
||||
[ children
|
||||
[ class InputContainer [ borderColor Colors.red ]
|
||||
, class Css.Selectize.SelectizeContainer [ borderColor Colors.red ]
|
||||
]
|
||||
, descendants
|
||||
[ input
|
||||
[ color Colors.red
|
||||
]
|
||||
]
|
||||
]
|
||||
, tbody
|
||||
[ descendants
|
||||
[ td
|
||||
[ textAlign right
|
||||
, whiteSpace noWrap
|
||||
]
|
||||
, td
|
||||
[ firstChild
|
||||
[ fontWeight normal
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, thead
|
||||
[ fontWeight bold
|
||||
, textAlign left
|
||||
]
|
||||
, class MultiDisplay
|
||||
[ backgroundColor Colors.darkerLightGrey
|
||||
, borderLeft3 (px 3) solid Colors.lightGrey
|
||||
, borderRight3 (px 3) solid Colors.lightGrey
|
||||
, borderRadius (px 3)
|
||||
]
|
||||
, th
|
||||
[ padding2 (px 3) (px 4)
|
||||
, textAlign center
|
||||
]
|
||||
, class ConfigTableGlobalRow
|
||||
[ descendants
|
||||
[ td
|
||||
[ firstChild
|
||||
[ fontWeight bold
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, class TextCell
|
||||
[ textAlign left ]
|
||||
, class ShortCell
|
||||
[ minWidth (em 5) ]
|
||||
, class MediumCell
|
||||
[ minWidth (em 10) ]
|
||||
, class LongCell
|
||||
[ minWidth (em 20) ]
|
||||
]
|
||||
]
|
||||
, class Saving
|
||||
[ fontSize (px 18)
|
||||
, fontWeight normal
|
||||
, textAlign right
|
||||
]
|
||||
, class NavBar
|
||||
[ margin zero
|
||||
, padding4 zero zero (px 110) zero
|
||||
, backgroundColor Colors.darkGrey
|
||||
, fontSize (px 18)
|
||||
, width (em 15)
|
||||
, maxWidth (em 15)
|
||||
, minWidth (em 15)
|
||||
, height (pct 100)
|
||||
, descendants
|
||||
[ class NavBarRoute
|
||||
[ height (px 60)
|
||||
, display block
|
||||
, lineHeight (px 60)
|
||||
, padding2 (px 0) (px 20)
|
||||
, colorize darkGreyScheme
|
||||
]
|
||||
, class NavBarCategory
|
||||
[ height (px 60)
|
||||
, display block
|
||||
, lineHeight (px 60)
|
||||
, padding2 (px 0) (px 20)
|
||||
, colorize darkGreyScheme
|
||||
]
|
||||
, class InvalidGroup
|
||||
[ color Colors.red |> important ]
|
||||
, class NavBarCategoryContainer
|
||||
[ descendants
|
||||
[ class NavBarRoute
|
||||
[ colorize darkGreyScheme
|
||||
, padding4 zero (px 20) zero (px 30)
|
||||
, fontWeight (int 500)
|
||||
, property "animation" "fadein 0.8s"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
module Css.Selectize exposing (..)
|
||||
|
||||
import Css exposing (..)
|
||||
import Css.LocalColors as Colors
|
||||
import Selectize
|
||||
import Css.Admin exposing (className)
|
||||
import Css.Elements exposing (input)
|
||||
import Css.Classes as C
|
||||
|
||||
codeFonts : List String
|
||||
codeFonts =
|
||||
[ "Inconsolata", "monospace" ]
|
||||
|
||||
|
||||
component : Style
|
||||
component =
|
||||
batch
|
||||
[ borderRadius (px 3)
|
||||
, position relative
|
||||
, margin zero
|
||||
, descendants
|
||||
[ class NoOptions
|
||||
[ backgroundColor Colors.lighterLightGrey
|
||||
, fontSize (px 14)
|
||||
, fontWeight (int 500)
|
||||
, color Colors.sandstone
|
||||
, padding (px 5)
|
||||
, textAlign center
|
||||
, cursor default
|
||||
, property "-webkit-user-select" "none"
|
||||
]
|
||||
, class SelectBox
|
||||
[ displayFlex
|
||||
, alignItems center
|
||||
, padding2 zero (px 5)
|
||||
, property "background-color" "inherit"
|
||||
, width (px 60)
|
||||
]
|
||||
, class BoxContainer
|
||||
[ position absolute
|
||||
, property "z-index" "100"
|
||||
, left (px -3)
|
||||
, backgroundColor Colors.white
|
||||
, textAlign left
|
||||
, fontWeight (int 500)
|
||||
, fontSize (pct 80)
|
||||
, borderRadius (px 3)
|
||||
, backgroundColor Colors.white
|
||||
, border3 (px 2) solid Colors.darkerLightGrey
|
||||
, borderTop zero
|
||||
, color Colors.sandstone
|
||||
, width (em 15)
|
||||
, cursor pointer
|
||||
, padding (px 5)
|
||||
]
|
||||
, class BoxItems
|
||||
[]
|
||||
, class BoxItemActive
|
||||
[ color Colors.cobalt
|
||||
, fontWeight (int 900)
|
||||
]
|
||||
, class BoxItem
|
||||
[ padding2 (px 3) (px 6)
|
||||
, overflow hidden
|
||||
, textOverflow ellipsis
|
||||
]
|
||||
, class Info
|
||||
[ padding2 (px 3) (px 6)
|
||||
, color Colors.darkGrey
|
||||
]
|
||||
, class MultiItemContainer
|
||||
[ descendants
|
||||
[ class SelectedItem
|
||||
[ backgroundColor Colors.cobalt
|
||||
, color Colors.white
|
||||
, padding (px 2)
|
||||
, margin2 zero (px 1)
|
||||
, fontFamilies codeFonts
|
||||
, fontSize (pct 70)
|
||||
, fontWeight normal
|
||||
, borderRadius (px 3)
|
||||
]
|
||||
, class FallbackItem
|
||||
[ backgroundColor Colors.amazonite
|
||||
]
|
||||
]
|
||||
]
|
||||
, class SingleItemContainer
|
||||
[ descendants
|
||||
[ class SelectedItem
|
||||
[ fontFamilies codeFonts
|
||||
, fontSize (px 14)
|
||||
, padding zero
|
||||
, borderRadius zero
|
||||
]
|
||||
, class FallbackItem
|
||||
[ color Colors.sandstone
|
||||
]
|
||||
]
|
||||
]
|
||||
, class C.SelectizeLanguage
|
||||
[ descendants
|
||||
[ class SelectBox
|
||||
[ width (px 140)
|
||||
]
|
||||
]
|
||||
]
|
||||
, class C.SelectizeCryptoCurrency
|
||||
[ descendants
|
||||
[ class SelectBox
|
||||
[ width (px 150)
|
||||
]
|
||||
]
|
||||
]
|
||||
, input
|
||||
[ textAlign left
|
||||
, property "background-color" "inherit"
|
||||
, padding2 (px 6) (px 2)
|
||||
, width (em 6)
|
||||
, cursor default
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
type Class
|
||||
= SelectizeContainer
|
||||
| SelectBox
|
||||
| BoxItems
|
||||
| BoxItem
|
||||
| BoxItemActive
|
||||
| SelectedItems
|
||||
| FallbackItems
|
||||
| FallbackItem
|
||||
| SelectedItem
|
||||
| InputEditing
|
||||
| SingleItemContainer
|
||||
| MultiItemContainer
|
||||
| BoxContainer
|
||||
| Info
|
||||
| InfoNoMatches
|
||||
| NoOptions
|
||||
| Disabled
|
||||
|
||||
|
||||
classes : Selectize.HtmlClasses
|
||||
classes =
|
||||
{ container = className SelectizeContainer
|
||||
, singleItemContainer = className SingleItemContainer
|
||||
, multiItemContainer = className MultiItemContainer
|
||||
, selectBox = className SelectBox
|
||||
, selectedItems = className SelectedItems
|
||||
, fallbackItems = className FallbackItems
|
||||
, fallbackItem = className FallbackItem
|
||||
, selectedItem = className SelectedItem
|
||||
, boxContainer = className BoxContainer
|
||||
, boxItems = className BoxItems
|
||||
, boxItem = className BoxItem
|
||||
, boxItemActive = className BoxItemActive
|
||||
, info = className Info
|
||||
, infoNoMatches = className InfoNoMatches
|
||||
, inputEditing = className InputEditing
|
||||
, noOptions = className NoOptions
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
module Customer.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import Common.Customer.Decoder exposing (customerDecoder)
|
||||
import Common.Customer.Types exposing (..)
|
||||
import Customer.Types exposing (..)
|
||||
|
||||
|
||||
patchCustomer : String -> String -> Authorized -> Cmd Msg
|
||||
patchCustomer id field value =
|
||||
patch ("/api/customer/" ++ id ++ "?" ++ field ++ "=" ++ authorizedToString value)
|
||||
|> withExpect (Http.expectJson customerDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
getCustomer : String -> Cmd Msg
|
||||
getCustomer id =
|
||||
get ("/api/customer/" ++ id)
|
||||
|> withExpect (Http.expectJson customerDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
module Customer.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Customer.Rest exposing (..)
|
||||
import Customer.Types exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
load : String -> ( Model, Cmd Msg )
|
||||
load id =
|
||||
( Loading, getCustomer id )
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load loadedModel ->
|
||||
loadedModel ! []
|
||||
|
||||
PatchCustomer id fieldName value ->
|
||||
model ! [ patchCustomer id fieldName value ]
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
module Customer.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Common.Customer.Types exposing (..)
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData Customer
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
| PatchCustomer String String Authorized
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
module Customer.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import RemoteData exposing (..)
|
||||
import Css.Admin as CSSAdmin exposing (..)
|
||||
import Css.Classes as C
|
||||
import Common.Customer.Types exposing (..)
|
||||
import Customer.Types exposing (..)
|
||||
import Date exposing (..)
|
||||
import Date.Extra exposing (toFormattedString)
|
||||
|
||||
|
||||
customerActions : String -> Authorized -> Html Msg
|
||||
customerActions id authorizedOverride =
|
||||
case authorizedOverride of
|
||||
Blocked ->
|
||||
button [ onClick (PatchCustomer id "authorizedOverride" Verified) ] [ text "Unblock" ]
|
||||
|
||||
Verified ->
|
||||
button [ onClick (PatchCustomer id "authorizedOverride" Blocked) ] [ text "Block" ]
|
||||
|
||||
Automatic ->
|
||||
button [ onClick (PatchCustomer id "authorizedOverride" Blocked) ] [ text "Block" ]
|
||||
|
||||
|
||||
formatDate : Maybe Date -> String
|
||||
formatDate date =
|
||||
case date of
|
||||
Just date ->
|
||||
toFormattedString "yyyy-MM-dd HH:mm" date
|
||||
|
||||
Nothing ->
|
||||
""
|
||||
|
||||
|
||||
maybeText : Maybe String -> Html Msg
|
||||
maybeText maybeString =
|
||||
text (Maybe.withDefault "" maybeString)
|
||||
|
||||
|
||||
actions : String -> String -> Authorized -> Html Msg
|
||||
actions id fieldKey checkedValue =
|
||||
(div []
|
||||
[ div []
|
||||
[ radio fieldKey checkedValue Automatic (PatchCustomer id fieldKey Automatic)
|
||||
, radio fieldKey checkedValue Blocked (PatchCustomer id fieldKey Blocked)
|
||||
, radio fieldKey checkedValue Verified (PatchCustomer id fieldKey Verified)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
radio : String -> Authorized -> Authorized -> msg -> Html msg
|
||||
radio inputName checkedValue value msg =
|
||||
label
|
||||
[ style [ ( "padding", "5px" ) ] ]
|
||||
[ input [ checked (checkedValue == value), type_ "radio", name inputName, onClick msg ] []
|
||||
, text (authorizedToString value)
|
||||
]
|
||||
|
||||
|
||||
verifyStatus : Maybe a -> Authorized -> Html Msg
|
||||
verifyStatus complianceType fieldOverride =
|
||||
if fieldOverride == Verified || (complianceType /= Nothing && fieldOverride == Automatic) then
|
||||
text "Verified"
|
||||
else
|
||||
text "Unverified"
|
||||
|
||||
|
||||
customerView : Customer -> Html Msg
|
||||
customerView customer =
|
||||
div []
|
||||
[ h1 [] [ text "Customer Details" ]
|
||||
, table [ CSSAdmin.class [ C.TxTable ] ]
|
||||
[ tbody []
|
||||
[ tr []
|
||||
[ td [] [ text "Customer ID" ]
|
||||
, td [] [ text customer.id ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Name" ]
|
||||
, td [] [ maybeText customer.name ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Phone" ]
|
||||
, td [] [ maybeText customer.phone ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Completed phone at" ]
|
||||
, td [] [ text (formatDate customer.phoneAt) ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Created" ]
|
||||
, td [] [ text (toFormattedString "yyyy-MM-dd HH:mm" customer.created) ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Block Customer" ]
|
||||
, td []
|
||||
[ customerActions customer.id customer.authorizedOverride ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Authorized at " ]
|
||||
, td [] [ text (formatDate customer.authorizedAt) ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Daily Volume " ]
|
||||
, td [] [ maybeText customer.dailyVolume ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, h2 [] [ text "Compliance types" ]
|
||||
, table [ CSSAdmin.class [ C.TxTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [] [ text "Name" ]
|
||||
, td [] [ text "Date" ]
|
||||
, td [] [ text "Verify Status" ]
|
||||
, td [] [ text "Override Status" ]
|
||||
, td [] [ text "User who overrode" ]
|
||||
, td [] [ text "Actions" ]
|
||||
]
|
||||
]
|
||||
, tbody []
|
||||
[ tr []
|
||||
[ td [] [ text "SMS" ]
|
||||
, td [] [ text (formatDate customer.phoneAt) ]
|
||||
, td [] [ verifyStatus customer.phone customer.smsOverride ]
|
||||
, td [] [ text (authorizedToString customer.smsOverride) ]
|
||||
, td [] [ maybeText customer.smsOverrideByName ]
|
||||
, td [] [ actions customer.id "smsOverride" customer.smsOverride ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "ID Card Data" ]
|
||||
, td [] [ text (formatDate customer.idCardDataAt) ]
|
||||
, td [] [ verifyStatus customer.idCardData customer.idCardDataOverride ]
|
||||
, td [] [ text (authorizedToString customer.idCardDataOverride) ]
|
||||
, td [] [ maybeText customer.idCardDataOverrideByName ]
|
||||
, td [] [ actions customer.id "idCardDataOverride" customer.idCardDataOverride ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "ID Card Photo" ]
|
||||
, td [] [ text (formatDate customer.idCardPhotoAt) ]
|
||||
, td [] [ verifyStatus customer.idCardPhotoPath customer.idCardPhotoOverride ]
|
||||
, td [] [ text (authorizedToString customer.idCardPhotoOverride) ]
|
||||
, td [] [ maybeText customer.idCardPhotoOverrideByName ]
|
||||
, td [] [ actions customer.id "idCardPhotoOverride" customer.idCardPhotoOverride ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Front Facing Camera" ]
|
||||
, td [] [ text (formatDate customer.frontCameraAt) ]
|
||||
, td [] [ verifyStatus customer.frontCameraPath customer.frontCameraOverride ]
|
||||
, td [] [ text (authorizedToString customer.frontCameraOverride) ]
|
||||
, td [] [ maybeText customer.frontCameraOverrideByName ]
|
||||
, td [] [ actions customer.id "frontCameraOverride" customer.frontCameraOverride ]
|
||||
]
|
||||
, tr []
|
||||
[ td [] [ text "Sanctions Check" ]
|
||||
, td [] [ text (formatDate customer.sanctionsAt) ]
|
||||
, td [] [ verifyStatus customer.sanctions customer.sanctionsOverride ]
|
||||
, td [] [ text (authorizedToString customer.sanctionsOverride) ]
|
||||
, td [] [ maybeText customer.sanctionsOverrideByName ]
|
||||
, td [] [ actions customer.id "sanctionsOverride" customer.sanctionsOverride ]
|
||||
]
|
||||
]
|
||||
]
|
||||
, h2 [] [ text "ID Card Photo" ]
|
||||
, case customer.idCardPhotoPath of
|
||||
Nothing ->
|
||||
text "N/A"
|
||||
|
||||
Just idCardPhotoPath ->
|
||||
div []
|
||||
[ img
|
||||
[ src ("/id-card-photo/" ++ idCardPhotoPath)
|
||||
, height 200
|
||||
, alt "N/A"
|
||||
] []
|
||||
]
|
||||
, h2 [] [ text "Front Facing Camera Photo" ]
|
||||
, case customer.frontCameraPath of
|
||||
Nothing ->
|
||||
text "N/A"
|
||||
|
||||
Just frontCameraPath ->
|
||||
div []
|
||||
[ img
|
||||
[ src ("/front-camera-photo/" ++ frontCameraPath)
|
||||
, height 200
|
||||
, alt "N/A"
|
||||
] []
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success customer ->
|
||||
div [] [ customerView customer ]
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
module Customers.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import Common.Customer.Decoder exposing (customersDecoder)
|
||||
import Customers.Types exposing (..)
|
||||
|
||||
|
||||
getCustomers : Cmd Msg
|
||||
getCustomers =
|
||||
get ("/api/customers")
|
||||
|> withExpect (Http.expectJson customersDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
module Customers.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Customers.Rest exposing (..)
|
||||
import Customers.Types exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
loadCmd : Cmd Msg
|
||||
loadCmd =
|
||||
getCustomers
|
||||
|
||||
|
||||
load : ( Model, Cmd Msg )
|
||||
load =
|
||||
( Loading, loadCmd )
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load loadedModel ->
|
||||
loadedModel ! []
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
module Customers.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Common.Customer.Types exposing (..)
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData Customers
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
module Customers.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (colspan, href)
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import RemoteData exposing (..)
|
||||
import List
|
||||
import Common.Customer.Types exposing (..)
|
||||
import Customers.Types exposing (..)
|
||||
import Date.Extra exposing (toFormattedString)
|
||||
|
||||
|
||||
customerLink : String -> Html Msg
|
||||
customerLink id =
|
||||
a [ href ("/#customer/" ++ id) ] [ text (String.left 8 id) ]
|
||||
|
||||
|
||||
maybeText : Maybe String -> Html Msg
|
||||
maybeText maybeString =
|
||||
text (Maybe.withDefault "" maybeString)
|
||||
|
||||
|
||||
rowView : Customer -> Html Msg
|
||||
rowView customer =
|
||||
tr [ class [] ]
|
||||
[ td [] [ customerLink customer.id ]
|
||||
, td [] [ text (toFormattedString "yyyy-MM-dd HH:mm" customer.created) ]
|
||||
, td [] [ maybeText customer.phone ]
|
||||
, td [] [ maybeText customer.name ]
|
||||
, td [] [ maybeText customer.status ]
|
||||
]
|
||||
|
||||
|
||||
tableView : Customers -> Html Msg
|
||||
tableView customers =
|
||||
if List.isEmpty customers then
|
||||
div [] [ text "No customers yet." ]
|
||||
else
|
||||
div []
|
||||
[ h1 [] [ text "Customers" ]
|
||||
, table [ class [ C.TxTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [] [ text "Id" ]
|
||||
, td [] [ text "Created" ]
|
||||
, td [] [ text "Phone" ]
|
||||
, td [] [ text "Name" ]
|
||||
, td [] [ text "Status" ]
|
||||
]
|
||||
]
|
||||
, tbody [] (List.map rowView customers)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success customers ->
|
||||
div [] [ tableView customers ]
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
module FieldSet.Rest exposing (..)
|
||||
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
import FieldSet.Types exposing (..)
|
||||
|
||||
|
||||
fieldPasswordDecoder : Bool -> FieldValue
|
||||
fieldPasswordDecoder present =
|
||||
if present then
|
||||
FieldPassword PasswordHidden
|
||||
else
|
||||
FieldPassword PasswordEmpty
|
||||
|
||||
|
||||
badInt : D.Decoder Int
|
||||
badInt =
|
||||
D.oneOf [ D.int ]
|
||||
|
||||
|
||||
fieldValueDecoder : String -> D.Decoder FieldValue
|
||||
fieldValueDecoder fieldType =
|
||||
case fieldType of
|
||||
"string" ->
|
||||
D.map FieldString D.string
|
||||
|
||||
"password" ->
|
||||
D.map fieldPasswordDecoder D.bool
|
||||
|
||||
"integer" ->
|
||||
D.map FieldInteger badInt
|
||||
|
||||
_ ->
|
||||
D.fail ("Unsupported field type: " ++ fieldType)
|
||||
|
||||
|
||||
fieldDecoder : D.Decoder Field
|
||||
fieldDecoder =
|
||||
(D.field "fieldType" D.string)
|
||||
|> D.andThen
|
||||
(\fieldType ->
|
||||
D.map6 Field
|
||||
(D.field "code" D.string)
|
||||
(D.field "display" D.string)
|
||||
(D.oneOf [ D.field "placeholder" D.string, D.succeed "" ])
|
||||
(D.field "required" D.bool)
|
||||
(D.field "value" (fieldValueDecoder fieldType))
|
||||
(D.field "value" (fieldValueDecoder fieldType))
|
||||
)
|
||||
|
||||
|
||||
encodeFieldValue : FieldValue -> E.Value
|
||||
encodeFieldValue fieldValue =
|
||||
case fieldValue of
|
||||
FieldString value ->
|
||||
E.string value
|
||||
|
||||
FieldPassword value ->
|
||||
case value of
|
||||
Password s ->
|
||||
E.string s
|
||||
|
||||
_ ->
|
||||
E.null
|
||||
|
||||
FieldInteger value ->
|
||||
E.int value
|
||||
|
||||
|
||||
maybeString : Maybe String -> E.Value
|
||||
maybeString maybeString =
|
||||
case maybeString of
|
||||
Nothing ->
|
||||
E.null
|
||||
|
||||
Just s ->
|
||||
E.string s
|
||||
|
||||
|
||||
encodeField : Field -> Maybe E.Value
|
||||
encodeField field =
|
||||
if isDirty field then
|
||||
Just
|
||||
(E.object
|
||||
[ ( "code", E.string field.code )
|
||||
, ( "value", encodeFieldValue field.value )
|
||||
]
|
||||
)
|
||||
else
|
||||
Nothing
|
||||
|
||||
|
||||
isDirty : Field -> Bool
|
||||
isDirty field =
|
||||
field.value /= field.loadedValue
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
module FieldSet.State exposing (update)
|
||||
|
||||
import FieldSet.Types exposing (..)
|
||||
|
||||
|
||||
updateField : String -> String -> Field -> Field
|
||||
updateField fieldCode fieldValueString field =
|
||||
if .code field == fieldCode then
|
||||
{ field | value = updateFieldValue fieldValueString field.value }
|
||||
else
|
||||
field
|
||||
|
||||
|
||||
updateFieldSet : String -> String -> List Field -> List Field
|
||||
updateFieldSet fieldCode fieldValueString fields =
|
||||
List.map (updateField fieldCode fieldValueString) fields
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Input fieldCode valueString ->
|
||||
updateFieldSet fieldCode valueString model ! []
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
module FieldSet.Types exposing (..)
|
||||
|
||||
|
||||
type alias Model =
|
||||
List Field
|
||||
|
||||
|
||||
type Msg
|
||||
= Input String String
|
||||
|
||||
|
||||
type alias Field =
|
||||
{ code : String
|
||||
, display : String
|
||||
, placeholder : String
|
||||
, required : Bool
|
||||
, value : FieldValue
|
||||
, loadedValue : FieldValue
|
||||
}
|
||||
|
||||
|
||||
type FieldPasswordType
|
||||
= Password String
|
||||
| PasswordEmpty
|
||||
| PasswordHidden
|
||||
|
||||
|
||||
type FieldValue
|
||||
= FieldString String
|
||||
| FieldPassword FieldPasswordType
|
||||
| FieldInteger Int
|
||||
|
||||
|
||||
updateFieldValue : String -> FieldValue -> FieldValue
|
||||
updateFieldValue stringValue oldFieldValue =
|
||||
case oldFieldValue of
|
||||
FieldString _ ->
|
||||
FieldString stringValue
|
||||
|
||||
FieldPassword _ ->
|
||||
FieldPassword (Password stringValue)
|
||||
|
||||
FieldInteger oldValue ->
|
||||
FieldInteger <| Result.withDefault oldValue <| String.toInt stringValue
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
module FieldSet.View exposing (view)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes as HA exposing (defaultValue, name, type_, placeholder)
|
||||
import Html.Events exposing (..)
|
||||
import FieldSet.Types exposing (..)
|
||||
import List
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
|
||||
|
||||
fieldComponent : Field -> Html Msg
|
||||
fieldComponent field =
|
||||
let
|
||||
inputEl =
|
||||
case field.value of
|
||||
FieldString string ->
|
||||
input
|
||||
[ onInput (Input field.code), placeholder field.placeholder, defaultValue string ]
|
||||
[]
|
||||
|
||||
FieldPassword pass ->
|
||||
case pass of
|
||||
PasswordEmpty ->
|
||||
input
|
||||
[ onInput (Input field.code), name field.code, type_ "password" ]
|
||||
[]
|
||||
|
||||
_ ->
|
||||
input
|
||||
[ onInput (Input field.code), name field.code, type_ "password", placeholder "••• Field is set •••" ]
|
||||
[]
|
||||
|
||||
FieldInteger int ->
|
||||
input
|
||||
[ onInput (Input field.code), type_ "number", defaultValue (toString int) ]
|
||||
[]
|
||||
in
|
||||
label []
|
||||
[ div [] [ text field.display ]
|
||||
, inputEl
|
||||
]
|
||||
|
||||
|
||||
fieldView : Field -> Html Msg
|
||||
fieldView field =
|
||||
div [ class [ C.FormRow ] ] [ fieldComponent field ]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div [ class [ C.ConfigContainer ] ] (List.map fieldView model)
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
module FuzzyMatch exposing (match)
|
||||
|
||||
import String
|
||||
import Fuzzy
|
||||
import Tuple
|
||||
|
||||
|
||||
clean : String -> String
|
||||
clean s =
|
||||
String.trim s
|
||||
|> String.toLower
|
||||
|
||||
|
||||
type alias DisplayRec =
|
||||
{ code : String
|
||||
, display : String
|
||||
}
|
||||
|
||||
|
||||
score : String -> Int -> DisplayRec -> ( ( Int, Int ), DisplayRec )
|
||||
score needle index hay =
|
||||
let
|
||||
match keyword =
|
||||
Fuzzy.match [] [] needle keyword
|
||||
|> .score
|
||||
|
||||
score =
|
||||
List.map match ((String.split " " (clean hay.display)) ++ [ clean hay.code, clean hay.display ])
|
||||
|> List.minimum
|
||||
|> Maybe.withDefault
|
||||
10000
|
||||
in
|
||||
( ( score, index ), hay )
|
||||
|
||||
|
||||
match : String -> List DisplayRec -> List DisplayRec
|
||||
match rawString list =
|
||||
let
|
||||
s =
|
||||
clean rawString
|
||||
in
|
||||
if String.isEmpty s then
|
||||
list
|
||||
else
|
||||
List.indexedMap (score s) list
|
||||
|> List.sortBy Tuple.first
|
||||
|> List.filter (((>) 1100) << Tuple.first << Tuple.first)
|
||||
|> List.map Tuple.second
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
module Main exposing (..)
|
||||
|
||||
import Html exposing (Html, Attribute, a, div, hr, input, span, text, map)
|
||||
import Html.Attributes exposing (class)
|
||||
import Navigation
|
||||
import SupportLogs.Types
|
||||
import SupportLogs.State
|
||||
import SupportLogs.View
|
||||
import UrlParser exposing ((</>), s, string, top, parseHash)
|
||||
import Navigation exposing (newUrl, Location)
|
||||
import StatusTypes exposing (..)
|
||||
|
||||
|
||||
type Category
|
||||
= AccountCat
|
||||
| MachineSettingsCat
|
||||
| GlobalSettingsCat
|
||||
| MaintenanceCat
|
||||
|
||||
|
||||
type Route
|
||||
= SupportLogsRoute (Maybe String)
|
||||
| NotFoundRoute
|
||||
|
||||
|
||||
type Msg
|
||||
= SupportLogsMsg SupportLogs.Types.Msg
|
||||
| UrlChange Navigation.Location
|
||||
|
||||
|
||||
main : Program Never Model Msg
|
||||
main =
|
||||
Navigation.program UrlChange
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- URL PARSERS
|
||||
|
||||
|
||||
parseRoute : UrlParser.Parser (Route -> a) a
|
||||
parseRoute =
|
||||
UrlParser.oneOf
|
||||
[ UrlParser.map (\id -> SupportLogsRoute (Just id)) (s "support_logs" </> string)
|
||||
, UrlParser.map (SupportLogsRoute Nothing) (s "support_logs")
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ location : Location
|
||||
, supportLogs : SupportLogs.Types.Model
|
||||
, status : Maybe StatusRec
|
||||
, err : Maybe String
|
||||
}
|
||||
|
||||
|
||||
init : Location -> ( Model, Cmd Msg )
|
||||
init location =
|
||||
let
|
||||
model =
|
||||
{ location = location
|
||||
, supportLogs = SupportLogs.State.init
|
||||
, status = Nothing
|
||||
, err = Nothing
|
||||
}
|
||||
|
||||
( newModel, newCmd ) =
|
||||
urlUpdate location model
|
||||
in
|
||||
newModel ! [ newCmd ]
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
SupportLogsMsg supportLogsMsg ->
|
||||
let
|
||||
( supportLogsModel, cmd ) =
|
||||
SupportLogs.State.update supportLogsMsg model.supportLogs
|
||||
in
|
||||
{ model | supportLogs = supportLogsModel } ! [ Cmd.map SupportLogsMsg cmd ]
|
||||
|
||||
UrlChange location ->
|
||||
urlUpdate location model
|
||||
|
||||
|
||||
content : Model -> Route -> Html Msg
|
||||
content model route =
|
||||
case route of
|
||||
SupportLogsRoute _ ->
|
||||
map SupportLogsMsg (SupportLogs.View.view model.supportLogs)
|
||||
|
||||
NotFoundRoute ->
|
||||
div [] [ text ("No such route") ]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
let
|
||||
route =
|
||||
Maybe.withDefault NotFoundRoute (parseHash parseRoute model.location)
|
||||
|
||||
invalidConfigGroups =
|
||||
Maybe.map .invalidConfigGroups model.status
|
||||
|> Maybe.withDefault []
|
||||
in
|
||||
div [ class "lamassuAdminLayout" ]
|
||||
[ div
|
||||
[ class "lamassuAdminMain" ]
|
||||
[ div [ class "lamassuAdminContent" ]
|
||||
[ content model route ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
urlUpdate : Location -> Model -> ( Model, Cmd Msg )
|
||||
urlUpdate location model =
|
||||
let
|
||||
route =
|
||||
Maybe.withDefault NotFoundRoute (parseHash parseRoute location)
|
||||
in
|
||||
case route of
|
||||
SupportLogsRoute maybeId ->
|
||||
let
|
||||
( supportLogsModel, cmd ) =
|
||||
SupportLogs.State.load maybeId
|
||||
in
|
||||
{ model | location = location, supportLogs = supportLogsModel } ! [ Cmd.map SupportLogsMsg cmd ]
|
||||
|
||||
NotFoundRoute ->
|
||||
{ model | location = location } ! []
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[]
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
module ClientServerWebsocket exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import HttpBuilder exposing (..)
|
||||
|
||||
|
||||
-- Fetch stuff: different configurations for starters
|
||||
|
||||
|
||||
type alias NewsResponse =
|
||||
()
|
||||
|
||||
|
||||
type alias Msg =
|
||||
NewsResponse (WebData News)
|
||||
|
||||
|
||||
getNews : Cmd Msg
|
||||
getNews =
|
||||
Http.get decodeNews "/news"
|
||||
|> RemoteData.asCmd
|
||||
|> Cmd.map NewsResponse
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
module Logs.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import Common.Logs.Decoder exposing (logsDecoder, machinesDecoder, latestLogSnapshotDecoder)
|
||||
import Logs.Types exposing (..)
|
||||
|
||||
|
||||
getLogs : Maybe String -> Cmd Msg
|
||||
getLogs maybeId =
|
||||
Http.get ("/api/logs/" ++ (Maybe.withDefault "" maybeId)) logsDecoder
|
||||
|> RemoteData.sendRequest
|
||||
|> Cmd.map LoadLogs
|
||||
|
||||
|
||||
getMachines : Cmd Msg
|
||||
getMachines =
|
||||
Http.get "/api/machines/" machinesDecoder
|
||||
|> RemoteData.sendRequest
|
||||
|> Cmd.map LoadMachines
|
||||
|
||||
|
||||
shareLogs : String -> Cmd Msg
|
||||
shareLogs id =
|
||||
post ("/api/support_logs?deviceId=" ++ id)
|
||||
|> withExpect (Http.expectJson latestLogSnapshotDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map LoadSupportLog
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
module Logs.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Logs.Rest exposing (..)
|
||||
import Logs.Types exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ logs = NotAsked, machines = NotAsked, latestLogSnapshot = NotAsked }
|
||||
|
||||
|
||||
load : Maybe String -> ( Model, Cmd Msg )
|
||||
load maybeId =
|
||||
( { logs = Loading, machines = Loading, latestLogSnapshot = NotAsked }, getData maybeId )
|
||||
|
||||
|
||||
getData : Maybe String -> Cmd Msg
|
||||
getData maybeId =
|
||||
Cmd.batch [ getLogs maybeId, getMachines ]
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
LoadLogs response ->
|
||||
( { model | logs = response }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
LoadMachines response ->
|
||||
( { model | machines = response }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ShareLogs machine ->
|
||||
model ! [ shareLogs machine.deviceId ]
|
||||
|
||||
LoadSupportLog supportLog ->
|
||||
( { model | latestLogSnapshot = supportLog }
|
||||
, Cmd.none
|
||||
)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
module Logs.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Common.Logs.Types exposing (..)
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ logs : WebData Logs
|
||||
, machines : WebData Machines
|
||||
, latestLogSnapshot : WebData SupportLogSnapshot
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= LoadLogs (WebData Logs)
|
||||
| LoadMachines (WebData Machines)
|
||||
| ShareLogs Machine
|
||||
| LoadSupportLog (WebData SupportLogSnapshot)
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
module Logs.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (href)
|
||||
import Html.Events exposing (onClick)
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import RemoteData exposing (..)
|
||||
import List
|
||||
import Common.Logs.Types exposing (..)
|
||||
import Logs.Types exposing (..)
|
||||
import Date exposing (..)
|
||||
import Date.Extra exposing (toFormattedString)
|
||||
|
||||
|
||||
machineLink : Machine -> Html Msg
|
||||
machineLink machine =
|
||||
a [ href ("/#logs/" ++ machine.deviceId) ] [ text machine.name ]
|
||||
|
||||
|
||||
logsActions : Logs -> Html Msg
|
||||
logsActions logs =
|
||||
button [ onClick (ShareLogs logs.currentMachine) ] [ text "Share log snapshot" ]
|
||||
|
||||
|
||||
formatDate : Date -> String
|
||||
formatDate date =
|
||||
toFormattedString "yyyy-MM-dd HH:mm" date
|
||||
|
||||
|
||||
rowView : Log -> Html Msg
|
||||
rowView log =
|
||||
tr [ class [] ]
|
||||
[ td [] [ text (formatDate log.timestamp) ]
|
||||
, td [] [ text log.logLevel ]
|
||||
, td [] [ text log.message ]
|
||||
]
|
||||
|
||||
|
||||
machineRowView : Machine -> Html Msg
|
||||
machineRowView machine =
|
||||
tr [ class [] ]
|
||||
[ td [] [ machineLink machine ]
|
||||
]
|
||||
|
||||
|
||||
machineItemView : Machine -> Html Msg
|
||||
machineItemView machine =
|
||||
li [] [ machineLink machine ]
|
||||
|
||||
|
||||
machinesView : Machines -> Html Msg
|
||||
machinesView machines =
|
||||
if List.isEmpty machines then
|
||||
div [ class [ C.EmptyTable ] ] [ text "No paired machines." ]
|
||||
else
|
||||
div []
|
||||
[ div [ class [ C.TxTable ] ]
|
||||
[ ul [] (List.map machineItemView machines)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
machines : Model -> Html Msg
|
||||
machines model =
|
||||
case model.machines of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading machines ..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success machines ->
|
||||
div [] [ machinesView machines ]
|
||||
|
||||
|
||||
latestLogSnapshot : Model -> Html Msg
|
||||
latestLogSnapshot model =
|
||||
case model.latestLogSnapshot of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] []
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success latestLogSnapshot ->
|
||||
h4 [] [ text "✓ Saved latest snapshot" ]
|
||||
|
||||
|
||||
logsView : Logs -> Html Msg
|
||||
logsView logs =
|
||||
if List.isEmpty logs.logs then
|
||||
div [] [ text "No logs yet." ]
|
||||
else
|
||||
div []
|
||||
[ logsActions logs
|
||||
, table [ class [ C.TxTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [] [ text "Date" ]
|
||||
, td [] [ text "Level" ]
|
||||
, td [] [ text "Message" ]
|
||||
]
|
||||
]
|
||||
, tbody [] (List.map rowView logs.logs)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
logs : Model -> Html Msg
|
||||
logs model =
|
||||
case model.logs of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading logs..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success logs ->
|
||||
div []
|
||||
[ logsView logs
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div []
|
||||
[ h1 [] [ text "Latest Logs" ]
|
||||
, div [ class [ C.PaneWrapper ] ]
|
||||
[ div [ class [ C.LeftPane ] ]
|
||||
[ h2 [] [ text "Machines" ]
|
||||
, machines model
|
||||
]
|
||||
, div [ class [ C.ContentPane ] ]
|
||||
[ h2 [] [ text "Logs" ]
|
||||
, latestLogSnapshot model
|
||||
, logs model
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -1,479 +0,0 @@
|
|||
module Main exposing (..)
|
||||
|
||||
import Html exposing (Html, Attribute, a, div, hr, input, span, text, map)
|
||||
import Html.Attributes exposing (class)
|
||||
import Navigation
|
||||
import Pair
|
||||
import Account
|
||||
import Config
|
||||
import Transactions
|
||||
import Customers.Types
|
||||
import Customers.State
|
||||
import Customers.View
|
||||
import Logs.Types
|
||||
import Logs.State
|
||||
import Logs.View
|
||||
import SupportLogs.Types
|
||||
import SupportLogs.State
|
||||
import SupportLogs.View
|
||||
import Customer.Types
|
||||
import Customer.State
|
||||
import Customer.View
|
||||
import MaintenanceFunding.Types
|
||||
import NavBar exposing (..)
|
||||
import UrlParser exposing ((</>), s, string, top, parseHash)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import RemoteData
|
||||
import Navigation exposing (newUrl, Location)
|
||||
import CoreTypes exposing (Msg(..), Route(..), Category(..))
|
||||
import AccountsDecoder exposing (accountsDecoder)
|
||||
import StatusTypes exposing (..)
|
||||
import StatusDecoder exposing (..)
|
||||
import Time exposing (..)
|
||||
import Css.Admin
|
||||
import Css.Classes as C
|
||||
import Markdown
|
||||
import MaintenanceMachines.Types
|
||||
import MaintenanceMachines.State
|
||||
import MaintenanceMachines.View
|
||||
import MaintenanceFunding.Types
|
||||
import MaintenanceFunding.State
|
||||
import MaintenanceFunding.View
|
||||
import Transaction.Types
|
||||
import Transaction.State
|
||||
import Transaction.View
|
||||
|
||||
|
||||
main : Program Never Model Msg
|
||||
main =
|
||||
Navigation.program UrlChange
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- URL PARSERS
|
||||
|
||||
|
||||
parseRoute : UrlParser.Parser (Route -> a) a
|
||||
parseRoute =
|
||||
UrlParser.oneOf
|
||||
[ UrlParser.map AccountRoute (s "account" </> string)
|
||||
, UrlParser.map PairRoute (s "pair")
|
||||
, UrlParser.map (\config crypto -> ConfigRoute config (Just crypto)) (s "config" </> string </> string)
|
||||
, UrlParser.map (\config -> ConfigRoute config Nothing) (s "config" </> string)
|
||||
, UrlParser.map MaintenanceMachinesRoute (s "machines")
|
||||
, UrlParser.map (\crypto -> MaintenanceFundingRoute (Just crypto)) (s "funding" </> string)
|
||||
, UrlParser.map (MaintenanceFundingRoute Nothing) (s "funding")
|
||||
, UrlParser.map TransactionsRoute (s "transactions")
|
||||
, UrlParser.map TransactionRoute (s "transaction" </> string)
|
||||
, UrlParser.map CustomersRoute (s "customers")
|
||||
, UrlParser.map CustomerRoute (s "customer" </> string)
|
||||
, UrlParser.map (\id -> LogsRoute (Just id)) (s "logs" </> string)
|
||||
, UrlParser.map (LogsRoute Nothing) (s "logs")
|
||||
, UrlParser.map (\id -> SupportLogsRoute (Just id)) (s "support_logs" </> string)
|
||||
, UrlParser.map (SupportLogsRoute Nothing) (s "support_logs")
|
||||
, UrlParser.map (ConfigRoute "setup" Nothing) top
|
||||
]
|
||||
|
||||
|
||||
getAccounts : Cmd Msg
|
||||
getAccounts =
|
||||
get ("/api/accounts")
|
||||
|> withExpect (Http.expectJson accountsDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map (RemoteData.withDefault [])
|
||||
|> Cmd.map LoadAccounts
|
||||
|
||||
|
||||
getStatus : Cmd Msg
|
||||
getStatus =
|
||||
get ("/api/status/")
|
||||
|> withExpect (Http.expectJson statusDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map LoadStatus
|
||||
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ location : Location
|
||||
, pair : Pair.Model
|
||||
, account : Account.Model
|
||||
, config : Config.Model
|
||||
, maintenanceMachines : MaintenanceMachines.Types.Model
|
||||
, maintenanceFunding : MaintenanceFunding.Types.Model
|
||||
, transactions : Transactions.Model
|
||||
, transaction : Transaction.Types.Model
|
||||
, customers : Customers.Types.Model
|
||||
, customer : Customer.Types.Model
|
||||
, logs : Logs.Types.Model
|
||||
, supportLogs : SupportLogs.Types.Model
|
||||
, accounts : List ( String, String )
|
||||
, status : Maybe StatusRec
|
||||
, err : Maybe String
|
||||
}
|
||||
|
||||
|
||||
init : Location -> ( Model, Cmd Msg )
|
||||
init location =
|
||||
let
|
||||
model =
|
||||
{ location = location
|
||||
, account = Account.init
|
||||
, pair = Pair.init False
|
||||
, config = Config.init
|
||||
, maintenanceMachines = MaintenanceMachines.State.init
|
||||
, maintenanceFunding = MaintenanceFunding.State.init
|
||||
, transactions = Transactions.init
|
||||
, transaction = Transaction.State.init
|
||||
, customers = Customers.State.init
|
||||
, customer = Customer.State.init
|
||||
, logs = Logs.State.init
|
||||
, supportLogs = SupportLogs.State.init
|
||||
, accounts = []
|
||||
, status = Nothing
|
||||
, err = Nothing
|
||||
}
|
||||
|
||||
( newModel, newCmd ) =
|
||||
urlUpdate location model
|
||||
in
|
||||
newModel ! [ newCmd, getAccounts, getStatus ]
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
PairMsg pairMsg ->
|
||||
let
|
||||
( pairModel, cmd ) =
|
||||
Pair.update pairMsg model.pair
|
||||
in
|
||||
{ model | pair = pairModel } ! [ Cmd.map PairMsg cmd ]
|
||||
|
||||
AccountMsg accountMsg ->
|
||||
let
|
||||
( accountModel, cmd ) =
|
||||
Account.update accountMsg model.account
|
||||
in
|
||||
{ model | account = accountModel } ! [ Cmd.map AccountMsg cmd ]
|
||||
|
||||
ConfigMsg configMsg ->
|
||||
let
|
||||
( configModel, cmd ) =
|
||||
Config.update configMsg model.config
|
||||
|
||||
loaded =
|
||||
Config.loaded configMsg
|
||||
|
||||
extraCmds =
|
||||
if loaded then
|
||||
[ getAccounts, getStatus ]
|
||||
else
|
||||
[]
|
||||
in
|
||||
{ model | config = configModel } ! ([ Cmd.map ConfigMsg cmd ] ++ extraCmds)
|
||||
|
||||
MaintenanceMachinesMsg maintenanceMachinesMsg ->
|
||||
let
|
||||
( maintenanceMachines, cmd ) =
|
||||
MaintenanceMachines.State.update maintenanceMachinesMsg model.maintenanceMachines
|
||||
in
|
||||
{ model | maintenanceMachines = maintenanceMachines } ! [ Cmd.map MaintenanceMachinesMsg cmd ]
|
||||
|
||||
MaintenanceFundingMsg maintenanceFundingMsg ->
|
||||
let
|
||||
( maintenanceFunding, cmd ) =
|
||||
MaintenanceFunding.State.update maintenanceFundingMsg model.maintenanceFunding
|
||||
in
|
||||
{ model | maintenanceFunding = maintenanceFunding } ! [ Cmd.map MaintenanceFundingMsg cmd ]
|
||||
|
||||
TransactionsMsg transactionsMsg ->
|
||||
let
|
||||
( transactionsModel, cmd ) =
|
||||
Transactions.update transactionsMsg model.transactions
|
||||
in
|
||||
{ model | transactions = transactionsModel } ! [ Cmd.map TransactionsMsg cmd ]
|
||||
|
||||
TransactionMsg transactionMsg ->
|
||||
let
|
||||
( transaction, cmd ) =
|
||||
Transaction.State.update transactionMsg model.transaction
|
||||
in
|
||||
{ model | transaction = transaction } ! [ Cmd.map TransactionMsg cmd ]
|
||||
|
||||
CustomersMsg customersMsg ->
|
||||
let
|
||||
( customersModel, cmd ) =
|
||||
Customers.State.update customersMsg model.customers
|
||||
in
|
||||
{ model | customers = customersModel } ! [ Cmd.map CustomersMsg cmd ]
|
||||
|
||||
CustomerMsg customerMsg ->
|
||||
let
|
||||
( customerModel, cmd ) =
|
||||
Customer.State.update customerMsg model.customer
|
||||
in
|
||||
{ model | customer = customerModel } ! [ Cmd.map CustomerMsg cmd ]
|
||||
|
||||
LogsMsg logsMsg ->
|
||||
let
|
||||
( logsModel, cmd ) =
|
||||
Logs.State.update logsMsg model.logs
|
||||
in
|
||||
{ model | logs = logsModel } ! [ Cmd.map LogsMsg cmd ]
|
||||
|
||||
SupportLogsMsg supportLogsMsg ->
|
||||
let
|
||||
( supportLogsModel, cmd ) =
|
||||
SupportLogs.State.update supportLogsMsg model.supportLogs
|
||||
in
|
||||
{ model | supportLogs = supportLogsModel } ! [ Cmd.map SupportLogsMsg cmd ]
|
||||
|
||||
LoadAccounts accounts ->
|
||||
{ model | accounts = accounts } ! []
|
||||
|
||||
LoadStatus webStatus ->
|
||||
let
|
||||
newStatus =
|
||||
List.filterMap identity [ RemoteData.toMaybe webStatus, model.status ]
|
||||
|> List.head
|
||||
|
||||
serverStatus =
|
||||
Maybe.withDefault False <| Maybe.map (\status -> status.server.up) newStatus
|
||||
|
||||
newPair =
|
||||
Pair.updateStatus serverStatus model.pair
|
||||
|
||||
rates =
|
||||
Maybe.withDefault [] <| Maybe.map (\status -> status.server.rates) newStatus
|
||||
|
||||
newConfig =
|
||||
Config.updateRates rates model.config
|
||||
in
|
||||
{ model | status = newStatus, pair = newPair, config = newConfig } ! []
|
||||
|
||||
NewUrl url ->
|
||||
let
|
||||
( configModel, configCmd ) =
|
||||
Config.submitNoLoad model.config
|
||||
in
|
||||
{ model | config = configModel } ! [ Navigation.newUrl url, Cmd.map ConfigMsg configCmd ]
|
||||
|
||||
UrlChange location ->
|
||||
urlUpdate location model
|
||||
|
||||
Interval ->
|
||||
let
|
||||
route =
|
||||
Maybe.withDefault NotFoundRoute (parseHash parseRoute model.location)
|
||||
|
||||
extraCmds =
|
||||
if route == TransactionsRoute then
|
||||
[ Cmd.map TransactionsMsg Transactions.loadCmd ]
|
||||
else
|
||||
[]
|
||||
in
|
||||
model ! ([ getStatus ] ++ extraCmds)
|
||||
|
||||
WebSocketMsg msg ->
|
||||
model ! []
|
||||
|
||||
|
||||
content : Model -> Route -> Html Msg
|
||||
content model route =
|
||||
case route of
|
||||
PairRoute ->
|
||||
map PairMsg (Pair.view model.pair)
|
||||
|
||||
AccountRoute _ ->
|
||||
map AccountMsg (Account.view model.account)
|
||||
|
||||
ConfigRoute _ _ ->
|
||||
map ConfigMsg (Config.view model.config)
|
||||
|
||||
MaintenanceMachinesRoute ->
|
||||
map MaintenanceMachinesMsg (MaintenanceMachines.View.view model.maintenanceMachines)
|
||||
|
||||
MaintenanceFundingRoute _ ->
|
||||
map MaintenanceFundingMsg (MaintenanceFunding.View.view model.maintenanceFunding)
|
||||
|
||||
TransactionsRoute ->
|
||||
map TransactionsMsg (Transactions.view model.transactions)
|
||||
|
||||
TransactionRoute _ ->
|
||||
map TransactionMsg (Transaction.View.view model.transaction)
|
||||
|
||||
CustomersRoute ->
|
||||
map CustomersMsg (Customers.View.view model.customers)
|
||||
|
||||
CustomerRoute _ ->
|
||||
map CustomerMsg (Customer.View.view model.customer)
|
||||
|
||||
LogsRoute _ ->
|
||||
map LogsMsg (Logs.View.view model.logs)
|
||||
|
||||
SupportLogsRoute _ ->
|
||||
map SupportLogsMsg (SupportLogs.View.view model.supportLogs)
|
||||
|
||||
NotFoundRoute ->
|
||||
div [] [ text ("No such route") ]
|
||||
|
||||
|
||||
statusBar : Maybe StatusRec -> Html Msg
|
||||
statusBar maybeStatus =
|
||||
case maybeStatus of
|
||||
Nothing ->
|
||||
div [ Css.Admin.class [ C.StatusBar ] ] [ text "Loading ..." ]
|
||||
|
||||
Just status ->
|
||||
let
|
||||
serverStatus =
|
||||
if not status.server.wasConfigured then
|
||||
[ Markdown.toHtml [] "**lamassu-server** not configured yet" ]
|
||||
else if status.server.up then
|
||||
[ Markdown.toHtml [] ("**lamassu-server** is up **/** " ++ status.server.machineStatus) ]
|
||||
else
|
||||
case status.server.lastPing of
|
||||
Nothing ->
|
||||
[ Markdown.toHtml [] "**lamassu-server** not up yet" ]
|
||||
|
||||
Just lastPing ->
|
||||
[ Markdown.toHtml [] ("**lamassu-server** has been down for " ++ lastPing) ]
|
||||
in
|
||||
div [ Css.Admin.class [ C.StatusBar ] ] serverStatus
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
let
|
||||
route =
|
||||
Maybe.withDefault NotFoundRoute (parseHash parseRoute model.location)
|
||||
|
||||
invalidConfigGroups =
|
||||
Maybe.map .invalidConfigGroups model.status
|
||||
|> Maybe.withDefault []
|
||||
in
|
||||
div [ class "lamassuAdminLayout" ]
|
||||
[ div
|
||||
[ class "lamassuAdminMain" ]
|
||||
[ NavBar.view route invalidConfigGroups
|
||||
, div [ class "lamassuAdminContent" ]
|
||||
[ content model route ]
|
||||
]
|
||||
, statusBar model.status
|
||||
]
|
||||
|
||||
|
||||
urlUpdate : Location -> Model -> ( Model, Cmd Msg )
|
||||
urlUpdate location model =
|
||||
let
|
||||
route =
|
||||
Maybe.withDefault NotFoundRoute (parseHash parseRoute location)
|
||||
in
|
||||
case route of
|
||||
PairRoute ->
|
||||
case model.status of
|
||||
Just status ->
|
||||
{ model | location = location, pair = Pair.init status.server.up } ! []
|
||||
|
||||
Nothing ->
|
||||
{ model | location = location, pair = Pair.init False } ! []
|
||||
|
||||
AccountRoute account ->
|
||||
let
|
||||
( accountModel, cmd ) =
|
||||
Account.load account
|
||||
in
|
||||
{ model | location = location, account = accountModel } ! [ Cmd.map AccountMsg cmd ]
|
||||
|
||||
ConfigRoute config maybeCryptoCodeString ->
|
||||
let
|
||||
( configModel, cmd ) =
|
||||
Config.load model.config config maybeCryptoCodeString
|
||||
in
|
||||
{ model | location = location, config = configModel } ! [ Cmd.map ConfigMsg cmd ]
|
||||
|
||||
MaintenanceMachinesRoute ->
|
||||
let
|
||||
( maintenanceMachines, cmd ) =
|
||||
MaintenanceMachines.State.load
|
||||
in
|
||||
{ model | location = location, maintenanceMachines = maintenanceMachines }
|
||||
! [ Cmd.map MaintenanceMachinesMsg cmd ]
|
||||
|
||||
MaintenanceFundingRoute maybeCrypto ->
|
||||
let
|
||||
( maintenanceFunding, cmd ) =
|
||||
MaintenanceFunding.State.load maybeCrypto
|
||||
in
|
||||
{ model | location = location, maintenanceFunding = maintenanceFunding }
|
||||
! [ Cmd.map MaintenanceFundingMsg cmd ]
|
||||
|
||||
TransactionsRoute ->
|
||||
let
|
||||
( transactionsModel, cmd ) =
|
||||
Transactions.load
|
||||
in
|
||||
{ model | location = location, transactions = transactionsModel } ! [ Cmd.map TransactionsMsg cmd ]
|
||||
|
||||
CustomersRoute ->
|
||||
let
|
||||
( customersModel, cmd ) =
|
||||
Customers.State.load
|
||||
in
|
||||
{ model | location = location, customers = customersModel } ! [ Cmd.map CustomersMsg cmd ]
|
||||
|
||||
CustomerRoute id ->
|
||||
let
|
||||
( customerModel, cmd ) =
|
||||
Customer.State.load id
|
||||
in
|
||||
{ model | location = location, customer = customerModel } ! [ Cmd.map CustomerMsg cmd ]
|
||||
|
||||
LogsRoute maybeId ->
|
||||
let
|
||||
( logsModel, cmd ) =
|
||||
Logs.State.load maybeId
|
||||
in
|
||||
{ model | location = location, logs = logsModel } ! [ Cmd.map LogsMsg cmd ]
|
||||
|
||||
SupportLogsRoute maybeId ->
|
||||
let
|
||||
( supportLogsModel, cmd ) =
|
||||
SupportLogs.State.load maybeId
|
||||
in
|
||||
{ model | location = location, supportLogs = supportLogsModel } ! [ Cmd.map SupportLogsMsg cmd ]
|
||||
|
||||
TransactionRoute txId ->
|
||||
let
|
||||
( transaction, cmd ) =
|
||||
Transaction.State.load txId
|
||||
in
|
||||
{ model | location = location, transaction = transaction }
|
||||
! [ Cmd.map TransactionMsg cmd ]
|
||||
|
||||
NotFoundRoute ->
|
||||
{ model | location = location } ! []
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ every (5 * second) (\_ -> Interval)
|
||||
]
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
module MaintenanceFunding.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import HttpBuilder exposing (..)
|
||||
import Json.Decode as D
|
||||
import Json.Decode.Pipeline exposing (decode, required)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import MaintenanceFunding.Types exposing (..)
|
||||
|
||||
|
||||
getForm : Maybe String -> Cmd Msg
|
||||
getForm maybeCrypto =
|
||||
get ("/api/funding/" ++ (Maybe.withDefault "" maybeCrypto))
|
||||
|> withExpect (Http.expectJson subModelDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
cryptoDisplayDecoder : D.Decoder CryptoDisplay
|
||||
cryptoDisplayDecoder =
|
||||
decode CryptoDisplay
|
||||
|> required "cryptoCode" D.string
|
||||
|> required "display" D.string
|
||||
|
||||
|
||||
subModelDecoder : D.Decoder SubModel
|
||||
subModelDecoder =
|
||||
decode SubModel
|
||||
|> required "cryptoCode" D.string
|
||||
|> required "cryptoDisplays" (D.list cryptoDisplayDecoder)
|
||||
|> required "fundingAddress" D.string
|
||||
|> required "fundingAddressUrl" D.string
|
||||
|> required "confirmedBalance" D.string
|
||||
|> required "pending" D.string
|
||||
|> required "fiatConfirmedBalance" D.string
|
||||
|> required "fiatPending" D.string
|
||||
|> required "fiatCode" D.string
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
module MaintenanceFunding.State exposing (..)
|
||||
|
||||
import MaintenanceFunding.Rest exposing (..)
|
||||
import MaintenanceFunding.Types exposing (..)
|
||||
import RemoteData exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
load : Maybe String -> ( Model, Cmd Msg )
|
||||
load maybeCrypto =
|
||||
( Loading, getForm maybeCrypto )
|
||||
|
||||
|
||||
fundingUpdate : SubModel -> ( SubModel, Cmd Msg )
|
||||
fundingUpdate model =
|
||||
model ! []
|
||||
|
||||
|
||||
switchCrypto : String -> Model -> ( Model, Cmd Msg )
|
||||
switchCrypto crypto model =
|
||||
( Loading, getForm (Just crypto) )
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load newModel ->
|
||||
RemoteData.update fundingUpdate newModel
|
||||
|
||||
CryptoSwitch crypto ->
|
||||
switchCrypto crypto model
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
module MaintenanceFunding.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
|
||||
|
||||
type alias CryptoDisplay =
|
||||
{ cryptoCode : String
|
||||
, display : String
|
||||
}
|
||||
|
||||
|
||||
type alias SubModel =
|
||||
{ cryptoCode : String
|
||||
, cryptoDisplays : List CryptoDisplay
|
||||
, fundingAddress : String
|
||||
, fundingAddressUrl : String
|
||||
, confirmedBalance : String
|
||||
, pending : String
|
||||
, fiatConfirmedBalance : String
|
||||
, fiatPending : String
|
||||
, fiatCode : String
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData SubModel
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
| CryptoSwitch String
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
module MaintenanceFunding.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (href)
|
||||
import RemoteData exposing (..)
|
||||
import MaintenanceFunding.Types exposing (..)
|
||||
import QRCode
|
||||
import QRCode.ECLevel as ECLevel
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
|
||||
|
||||
qrCode : String -> Html msg
|
||||
qrCode s =
|
||||
let
|
||||
resultQRCode =
|
||||
QRCode.toSvgWithECLevel s ECLevel.L
|
||||
in
|
||||
case resultQRCode of
|
||||
Result.Ok view ->
|
||||
view
|
||||
|
||||
Result.Err err ->
|
||||
Html.text (toString err)
|
||||
|
||||
|
||||
fundingView : SubModel -> Html Msg
|
||||
fundingView subModel =
|
||||
div []
|
||||
[ cryptosView subModel.cryptoDisplays (Just subModel.cryptoCode)
|
||||
, section [ class [ C.Container ] ]
|
||||
[ div [] [ text ("Deposit " ++ subModel.cryptoCode ++ " to this address.") ]
|
||||
, div [] [ qrCode subModel.fundingAddressUrl ]
|
||||
, div [ class [ C.CryptoAddress ] ] [ text subModel.fundingAddress ]
|
||||
, section [ class [ C.BalanceSection ] ]
|
||||
[ h2 [] [ text "Balance" ]
|
||||
, div [] [ text (subModel.confirmedBalance ++ " " ++ subModel.cryptoCode ++ " (" ++ subModel.pending ++ " pending)") ]
|
||||
, div [] [ text (subModel.fiatConfirmedBalance ++ " " ++ subModel.fiatCode ++ " (" ++ subModel.fiatPending ++ " pending)") ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
cryptoView : Maybe String -> CryptoDisplay -> Html Msg
|
||||
cryptoView maybeActiveCrypto cryptoDisplay =
|
||||
let
|
||||
activeClass =
|
||||
case maybeActiveCrypto of
|
||||
Nothing ->
|
||||
class []
|
||||
|
||||
Just activeCrypto ->
|
||||
if (activeCrypto == cryptoDisplay.cryptoCode) then
|
||||
class [ C.Active ]
|
||||
else
|
||||
class []
|
||||
|
||||
url =
|
||||
"/#funding/" ++ cryptoDisplay.cryptoCode
|
||||
in
|
||||
a [ activeClass, class [ C.CryptoTab ], href url ] [ text cryptoDisplay.display ]
|
||||
|
||||
|
||||
cryptosView : List CryptoDisplay -> Maybe String -> Html Msg
|
||||
cryptosView cryptos activeCrypto =
|
||||
nav [ class [ C.CryptoTabs ] ] (List.map (cryptoView activeCrypto) cryptos)
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success subModel ->
|
||||
fundingView subModel
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
module MaintenanceMachines.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import HttpBuilder exposing (..)
|
||||
import Json.Decode as D
|
||||
import Json.Encode as E
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import MaintenanceMachines.Types exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
toModel : SavingStatus -> Machines -> SubModel
|
||||
toModel status machines =
|
||||
{ status = status, machines = machines }
|
||||
|
||||
|
||||
getForm : Cmd Msg
|
||||
getForm =
|
||||
get ("/api/machines")
|
||||
|> withExpect (Http.expectJson machinesDecoder)
|
||||
|> send (Result.map (toModel NotSaving) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
postForm : MachineAction -> Cmd Msg
|
||||
postForm action =
|
||||
post "/api/machines"
|
||||
|> withJsonBody (encodeAction action)
|
||||
|> withExpect (Http.expectJson machinesDecoder)
|
||||
|> send (Result.map (toModel Saved) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
machineDecoder : D.Decoder Machine
|
||||
machineDecoder =
|
||||
D.map7 Machine
|
||||
(D.field "deviceId" D.string)
|
||||
(D.field "name" D.string)
|
||||
(D.field "cashbox" D.int)
|
||||
(D.field "cassette1" D.int)
|
||||
(D.field "cassette2" D.int)
|
||||
(D.field "paired" D.bool)
|
||||
(D.field "cashOut" D.bool)
|
||||
|
||||
|
||||
machinesDecoder : D.Decoder Machines
|
||||
machinesDecoder =
|
||||
D.map identity
|
||||
(D.field "machines" (D.list machineDecoder))
|
||||
|
||||
|
||||
encodeAction : MachineAction -> E.Value
|
||||
encodeAction action =
|
||||
case action of
|
||||
ResetCashOutBills machine ->
|
||||
E.object
|
||||
[ ( "action", E.string "resetCashOutBills" )
|
||||
, ( "deviceId", E.string machine.deviceId )
|
||||
, ( "cassettes", E.list [ E.int machine.cassette1, E.int machine.cassette2 ] )
|
||||
]
|
||||
|
||||
UnpairMachine machine ->
|
||||
E.object
|
||||
[ ( "action", E.string "unpair" )
|
||||
, ( "deviceId", E.string machine.deviceId )
|
||||
]
|
||||
|
||||
RebootMachine machine ->
|
||||
E.object
|
||||
[ ( "action", E.string "reboot" )
|
||||
, ( "deviceId", E.string machine.deviceId )
|
||||
]
|
||||
|
||||
RestartServices machine ->
|
||||
E.object
|
||||
[ ( "action", E.string "restartServices" )
|
||||
, ( "deviceId", E.string machine.deviceId )
|
||||
]
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
module MaintenanceMachines.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import String
|
||||
import List
|
||||
import Process
|
||||
import Task
|
||||
import Time exposing (second)
|
||||
import MaintenanceMachines.Types exposing (..)
|
||||
import MaintenanceMachines.Rest exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
load : ( Model, Cmd Msg )
|
||||
load =
|
||||
( Loading, getForm )
|
||||
|
||||
|
||||
updateMachine : Machine -> Machine -> Machine
|
||||
updateMachine machine oldMachine =
|
||||
if machine.deviceId == oldMachine.deviceId then
|
||||
machine
|
||||
else
|
||||
oldMachine
|
||||
|
||||
|
||||
updateCassette : Machine -> Position -> String -> SubModel -> ( SubModel, Cmd Msg )
|
||||
updateCassette machine position str subModel =
|
||||
let
|
||||
countResult =
|
||||
String.toInt str
|
||||
|
||||
updatedMachine =
|
||||
case countResult of
|
||||
Ok count ->
|
||||
case position of
|
||||
Top ->
|
||||
{ machine | cassette1 = count }
|
||||
|
||||
Bottom ->
|
||||
{ machine | cassette2 = count }
|
||||
|
||||
Err _ ->
|
||||
machine
|
||||
|
||||
machines =
|
||||
List.map (updateMachine updatedMachine) subModel.machines
|
||||
in
|
||||
{ subModel | machines = machines } ! []
|
||||
|
||||
|
||||
updateAction : MachineAction -> SubModel -> ( SubModel, Cmd Msg )
|
||||
updateAction action subModel =
|
||||
subModel ! [ postForm action ]
|
||||
|
||||
|
||||
saveUpdate : SubModel -> ( SubModel, Cmd Msg )
|
||||
saveUpdate model =
|
||||
let
|
||||
cmd =
|
||||
if (model.status == Saved) then
|
||||
Process.sleep (2 * second)
|
||||
|> Task.perform (\_ -> HideSaveIndication)
|
||||
else
|
||||
Cmd.none
|
||||
in
|
||||
model ! [ cmd ]
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Action ->
|
||||
model ! []
|
||||
|
||||
Load newModel ->
|
||||
RemoteData.update saveUpdate newModel
|
||||
|
||||
InputCassette machine position str ->
|
||||
RemoteData.update (updateCassette machine position str) model
|
||||
|
||||
Submit action ->
|
||||
RemoteData.update (updateAction action) model
|
||||
|
||||
HideSaveIndication ->
|
||||
RemoteData.update (\subModel -> { subModel | status = NotSaving } ! []) model
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
module MaintenanceMachines.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
type alias SubModel =
|
||||
{ status : SavingStatus
|
||||
, machines : Machines
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData SubModel
|
||||
|
||||
|
||||
type alias Machine =
|
||||
{ deviceId : String
|
||||
, name : String
|
||||
, cashbox : Int
|
||||
, cassette1 : Int
|
||||
, cassette2 : Int
|
||||
, paired : Bool
|
||||
, cashOut : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias Machines =
|
||||
List Machine
|
||||
|
||||
|
||||
type MachineAction
|
||||
= ResetCashOutBills Machine
|
||||
| UnpairMachine Machine
|
||||
| RebootMachine Machine
|
||||
| RestartServices Machine
|
||||
|
||||
|
||||
type Msg
|
||||
= Action
|
||||
| Load Model
|
||||
| InputCassette Machine Position String
|
||||
| Submit MachineAction
|
||||
| HideSaveIndication
|
||||
|
||||
|
||||
type Position
|
||||
= Top
|
||||
| Bottom
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
module MaintenanceMachines.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (defaultValue)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import List
|
||||
import RemoteData exposing (..)
|
||||
import MaintenanceMachines.Types exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
inputCassetteView : Machine -> Position -> Int -> Html Msg
|
||||
inputCassetteView machine position count =
|
||||
input
|
||||
[ class [ C.BasicInput ]
|
||||
, onInput (InputCassette machine position)
|
||||
, defaultValue (toString count)
|
||||
]
|
||||
[]
|
||||
|
||||
|
||||
rowView : Machine -> Html Msg
|
||||
rowView machine =
|
||||
let
|
||||
resetBills =
|
||||
if machine.cashOut then
|
||||
td []
|
||||
[ button [ class [ C.TableButton ], onClick (Submit (ResetCashOutBills machine)) ] [ text "Update Counts" ]
|
||||
]
|
||||
else
|
||||
td [] []
|
||||
|
||||
actions =
|
||||
[ td []
|
||||
[ button [ class [ C.TableButton ], onClick (Submit (UnpairMachine machine)) ] [ text "Unpair" ] ]
|
||||
, td []
|
||||
[ button [ class [ C.TableButton ], onClick (Submit (RebootMachine machine)) ] [ text "Reboot OS" ] ]
|
||||
, td []
|
||||
[ button [ class [ C.TableButton ], onClick (Submit (RestartServices machine)) ] [ text "Restart Services" ] ]
|
||||
, resetBills
|
||||
]
|
||||
|
||||
cassetteCounts =
|
||||
if machine.cashOut then
|
||||
[ td []
|
||||
[ div [ classList [ ( C.Component, True ), ( C.FocusedComponent, False ) ] ]
|
||||
[ inputCassetteView machine Top machine.cassette1 ]
|
||||
]
|
||||
, td []
|
||||
[ div [ classList [ ( C.Component, True ), ( C.FocusedComponent, False ) ] ]
|
||||
[ inputCassetteView machine Bottom machine.cassette2 ]
|
||||
]
|
||||
]
|
||||
else
|
||||
[ td [ class [ C.CellDisabled ] ] [], td [ class [ C.CellDisabled ] ] [] ]
|
||||
in
|
||||
tr []
|
||||
([ td [] [ text machine.name ] ]
|
||||
++ cassetteCounts
|
||||
++ actions
|
||||
)
|
||||
|
||||
|
||||
tableView : Machines -> Html Msg
|
||||
tableView machines =
|
||||
if List.isEmpty machines then
|
||||
div [ class [ C.EmptyTable ] ] [ text "No paired machines." ]
|
||||
else
|
||||
table [ class [ C.ConfigTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [] []
|
||||
, td [] [ text "Top Bill Count" ]
|
||||
, td [] [ text "Bottom Bill Count" ]
|
||||
]
|
||||
]
|
||||
, tbody [] (List.map rowView machines)
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success subModel ->
|
||||
let
|
||||
statusString =
|
||||
case subModel.status of
|
||||
Saved ->
|
||||
"Saved"
|
||||
|
||||
_ ->
|
||||
""
|
||||
in
|
||||
div []
|
||||
[ div [ class [ C.SectionLabel ] ]
|
||||
[ div []
|
||||
[ div [ class [ C.ConfigContainer ] ]
|
||||
[ tableView subModel.machines
|
||||
, div [ class [ C.Saving ] ] [ text statusString ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -1,270 +0,0 @@
|
|||
module NavBar exposing (view, routeToUrl)
|
||||
|
||||
import Html exposing (Html, Attribute, a, div, hr, input, span, text, ul, li, nav)
|
||||
import Html.Events exposing (onClick)
|
||||
import Html.CssHelpers
|
||||
import Css.Classes
|
||||
import String
|
||||
import CoreTypes
|
||||
exposing
|
||||
( Msg(..)
|
||||
, Category(..)
|
||||
, Route(..)
|
||||
)
|
||||
|
||||
|
||||
{ id, class, classList } =
|
||||
Html.CssHelpers.withNamespace "lamassuAdmin"
|
||||
|
||||
|
||||
maybeUrl : String -> List (Maybe String) -> String
|
||||
maybeUrl root maybeStrings =
|
||||
List.filterMap identity maybeStrings
|
||||
|> List.append [ root ]
|
||||
|> String.join "/"
|
||||
|
||||
|
||||
routeToUrl : Route -> String
|
||||
routeToUrl route =
|
||||
case route of
|
||||
PairRoute ->
|
||||
"/#pair"
|
||||
|
||||
AccountRoute account ->
|
||||
"/#account/" ++ account
|
||||
|
||||
ConfigRoute configGroup maybeCrypto ->
|
||||
maybeUrl ("/#config/" ++ configGroup) [ maybeCrypto ]
|
||||
|
||||
MaintenanceMachinesRoute ->
|
||||
"/#machines/"
|
||||
|
||||
MaintenanceFundingRoute maybeCrypto ->
|
||||
maybeUrl ("/#funding") [ maybeCrypto ]
|
||||
|
||||
TransactionsRoute ->
|
||||
"/#transactions/"
|
||||
|
||||
TransactionRoute txId ->
|
||||
"/#transaction/" ++ txId
|
||||
|
||||
CustomersRoute ->
|
||||
"/#customers/"
|
||||
|
||||
CustomerRoute id ->
|
||||
"/#customer/" ++ id
|
||||
|
||||
LogsRoute maybeId ->
|
||||
"/#logs/" ++ (Maybe.withDefault "" maybeId)
|
||||
|
||||
SupportLogsRoute maybeId ->
|
||||
"/#support_logs/" ++ (Maybe.withDefault "" maybeId)
|
||||
|
||||
NotFoundRoute ->
|
||||
Debug.crash "Need unknown route"
|
||||
|
||||
|
||||
linkClasses : Route -> Route -> Bool -> Attribute msg
|
||||
linkClasses linkRoute route isValid =
|
||||
let
|
||||
validityClass =
|
||||
if isValid then
|
||||
[]
|
||||
else
|
||||
[ Css.Classes.InvalidGroup ]
|
||||
|
||||
active =
|
||||
case route of
|
||||
ConfigRoute config _ ->
|
||||
linkRoute == ConfigRoute config Nothing
|
||||
|
||||
MaintenanceFundingRoute _ ->
|
||||
linkRoute == MaintenanceFundingRoute Nothing
|
||||
|
||||
LogsRoute _ ->
|
||||
linkRoute == LogsRoute Nothing
|
||||
|
||||
_ ->
|
||||
linkRoute == route
|
||||
in
|
||||
if (active) then
|
||||
class ([ Css.Classes.NavBarRoute, Css.Classes.Active ] ++ validityClass)
|
||||
else
|
||||
class ([ Css.Classes.NavBarRoute ] ++ validityClass)
|
||||
|
||||
|
||||
type alias Link =
|
||||
( String, Route, Bool )
|
||||
|
||||
|
||||
activeCategory : Maybe Category -> Category -> Bool -> Attribute msg
|
||||
activeCategory maybeCurrentCategory linkedCategory isValid =
|
||||
let
|
||||
validityClass =
|
||||
if isValid then
|
||||
[]
|
||||
else
|
||||
[ Css.Classes.InvalidGroup ]
|
||||
in
|
||||
case maybeCurrentCategory of
|
||||
Nothing ->
|
||||
class ([ Css.Classes.NavBarCategory ] ++ validityClass)
|
||||
|
||||
Just currentCategory ->
|
||||
if currentCategory == linkedCategory then
|
||||
class ([ Css.Classes.NavBarCategory, Css.Classes.Active ] ++ validityClass)
|
||||
else
|
||||
class ([ Css.Classes.NavBarCategory ] ++ validityClass)
|
||||
|
||||
|
||||
categoryView : Maybe Category -> ( String, Category, Route, Bool ) -> Html Msg
|
||||
categoryView currentCategory link =
|
||||
let
|
||||
( desc, category, linkRoute, isValid ) =
|
||||
link
|
||||
in
|
||||
div
|
||||
[ onClick (NewUrl (routeToUrl linkRoute))
|
||||
, activeCategory currentCategory category isValid
|
||||
]
|
||||
[ text desc ]
|
||||
|
||||
|
||||
linkView : Maybe Category -> Route -> Maybe Category -> Link -> Html Msg
|
||||
linkView maybeCategory currentRoute maybeLinkedCategory link =
|
||||
let
|
||||
( desc, linkRoute, isValid ) =
|
||||
link
|
||||
in
|
||||
div [ onClick (NewUrl (routeToUrl linkRoute)), linkClasses linkRoute currentRoute isValid ] [ text desc ]
|
||||
|
||||
|
||||
linksView : Maybe Category -> Route -> ( String, Category, Route, Bool ) -> List Link -> Html Msg
|
||||
linksView maybeCurrentCategory currentRoute ( catDesc, cat, route, isValid ) links =
|
||||
if maybeCurrentCategory == (Just cat) then
|
||||
div [ class [ Css.Classes.NavBarCategoryContainer ] ]
|
||||
[ categoryView maybeCurrentCategory ( catDesc, cat, route, isValid )
|
||||
, div [] (List.map (linkView maybeCurrentCategory currentRoute (Just cat)) links)
|
||||
]
|
||||
else
|
||||
div [ class [ Css.Classes.NavBarCategoryContainer ] ]
|
||||
[ categoryView maybeCurrentCategory ( catDesc, cat, route, isValid )
|
||||
]
|
||||
|
||||
|
||||
determineConfigCategory : String -> Maybe Category
|
||||
determineConfigCategory configCode =
|
||||
if List.member configCode [ "definition", "setup", "cashOut", "commissions", "balanceAlerts" ] then
|
||||
Just MachineSettingsCat
|
||||
else if List.member configCode [ "walletSettings", "notifications", "compliance", "coinAtmRadar", "terms", "operatorInfo", "fudgeFactor" ] then
|
||||
Just GlobalSettingsCat
|
||||
else
|
||||
Nothing
|
||||
|
||||
|
||||
allClear : List String -> Category -> Bool
|
||||
allClear invalidGroups cat =
|
||||
not <| List.any (\groupCode -> determineConfigCategory groupCode == Just cat) invalidGroups
|
||||
|
||||
|
||||
determineCategory : Route -> Maybe Category
|
||||
determineCategory route =
|
||||
case route of
|
||||
AccountRoute account ->
|
||||
Just AccountCat
|
||||
|
||||
ConfigRoute config _ ->
|
||||
determineConfigCategory config
|
||||
|
||||
MaintenanceMachinesRoute ->
|
||||
Just MaintenanceCat
|
||||
|
||||
MaintenanceFundingRoute _ ->
|
||||
Just MaintenanceCat
|
||||
|
||||
PairRoute ->
|
||||
Nothing
|
||||
|
||||
TransactionsRoute ->
|
||||
Nothing
|
||||
|
||||
TransactionRoute _ ->
|
||||
Nothing
|
||||
|
||||
CustomersRoute ->
|
||||
Just MaintenanceCat
|
||||
|
||||
CustomerRoute _ ->
|
||||
Just MaintenanceCat
|
||||
|
||||
LogsRoute _ ->
|
||||
Just MaintenanceCat
|
||||
|
||||
SupportLogsRoute _ ->
|
||||
Nothing
|
||||
|
||||
NotFoundRoute ->
|
||||
Nothing
|
||||
|
||||
|
||||
view : Route -> List String -> Html Msg
|
||||
view route invalidGroups =
|
||||
let
|
||||
maybeCategory =
|
||||
determineCategory route
|
||||
|
||||
l =
|
||||
linkView maybeCategory route Nothing
|
||||
|
||||
ll =
|
||||
linksView maybeCategory route
|
||||
|
||||
isValid group =
|
||||
not (List.member group invalidGroups)
|
||||
|
||||
allClearMachine =
|
||||
allClear invalidGroups MachineSettingsCat
|
||||
|
||||
allClearGlobal =
|
||||
allClear invalidGroups GlobalSettingsCat
|
||||
|
||||
configLink code display =
|
||||
( display, ConfigRoute code Nothing, isValid code )
|
||||
in
|
||||
nav [ class [ Css.Classes.NavBar ] ]
|
||||
[ l ( "Transactions", TransactionsRoute, True )
|
||||
, ll ( "Maintenance", MaintenanceCat, MaintenanceMachinesRoute, True )
|
||||
[ ( "Machines", MaintenanceMachinesRoute, True )
|
||||
, ( "Funding", MaintenanceFundingRoute Nothing, True )
|
||||
, ( "Customers", CustomersRoute, True )
|
||||
, ( "Logs", LogsRoute Nothing, True )
|
||||
]
|
||||
, ll ( "Machine Settings", MachineSettingsCat, ConfigRoute "definition" Nothing, allClearMachine )
|
||||
[ configLink "definition" "Definition"
|
||||
, configLink "setup" "Setup"
|
||||
, configLink "cashOut" "Cash Out"
|
||||
, configLink "commissions" "Commissions"
|
||||
, configLink "balanceAlerts" "Balance Alerts"
|
||||
]
|
||||
, ll ( "Global Settings", GlobalSettingsCat, ConfigRoute "walletSettings" Nothing, allClearGlobal )
|
||||
[ configLink "walletSettings" "Wallet Settings"
|
||||
, configLink "notifications" "Notifications"
|
||||
, configLink "compliance" "Compliance"
|
||||
, configLink "coinAtmRadar" "Coin ATM Radar"
|
||||
, configLink "terms" "Terms and Conditions"
|
||||
, configLink "operatorInfo" "Operator Info"
|
||||
, configLink "fudgeFactor" "Fudge Factor"
|
||||
]
|
||||
, ll ( "Third Party Services", AccountCat, AccountRoute "bitgo", True )
|
||||
[ ( "BitGo", AccountRoute "bitgo", True )
|
||||
, ( "Bitstamp", AccountRoute "bitstamp", True )
|
||||
, ( "Blockcypher", AccountRoute "blockcypher", True )
|
||||
, ( "Infura", AccountRoute "infura", True )
|
||||
, ( "itBit", AccountRoute "itbit", True )
|
||||
, ( "Kraken", AccountRoute "kraken", True )
|
||||
, ( "Mailgun", AccountRoute "mailgun", True )
|
||||
, ( "Strike", AccountRoute "strike", True )
|
||||
, ( "Twilio", AccountRoute "twilio", True )
|
||||
]
|
||||
, l ( "+ Add Machine", PairRoute, True )
|
||||
]
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
module Pair exposing (..)
|
||||
|
||||
import Html exposing (Html, Attribute, h1, a, div, hr, input, span, text, node, button, strong, label)
|
||||
import Html.Attributes exposing (id, attribute, placeholder, disabled, style, size)
|
||||
import Html.Events exposing (onClick, onInput)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import String
|
||||
import RemoteData exposing (RemoteData(NotAsked, Loading, Failure, Success))
|
||||
import QRCode
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import QRCode.ECLevel as ECLevel
|
||||
|
||||
|
||||
-- MODEL
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ totem : RemoteData.WebData String
|
||||
, name : String
|
||||
, serverStatus : Bool
|
||||
}
|
||||
|
||||
|
||||
getTotem : String -> Cmd Msg
|
||||
getTotem name =
|
||||
get "/api/totem"
|
||||
|> withQueryParams [ ( "name", name ) ]
|
||||
|> withExpect Http.expectString
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
init : Bool -> Model
|
||||
init serverStatus =
|
||||
{ totem = RemoteData.NotAsked
|
||||
, name = ""
|
||||
, serverStatus = serverStatus
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= Load (RemoteData.WebData String)
|
||||
| InputName String
|
||||
| SubmitName
|
||||
|
||||
|
||||
updateStatus : Bool -> Model -> Model
|
||||
updateStatus isUp model =
|
||||
{ model | serverStatus = isUp }
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load webData ->
|
||||
let
|
||||
_ =
|
||||
Debug.log "TOTEM" (RemoteData.withDefault "Network Error" webData)
|
||||
in
|
||||
{ model | totem = webData } ! []
|
||||
|
||||
InputName name ->
|
||||
{ model | name = name } ! []
|
||||
|
||||
SubmitName ->
|
||||
model ! [ getTotem model.name ]
|
||||
|
||||
|
||||
qrCode : String -> Html msg
|
||||
qrCode s =
|
||||
let
|
||||
resultQRCode =
|
||||
QRCode.toSvgWithECLevel s ECLevel.L
|
||||
in
|
||||
case resultQRCode of
|
||||
Result.Ok view ->
|
||||
view
|
||||
|
||||
Result.Err err ->
|
||||
Html.text (toString err)
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
if model.serverStatus then
|
||||
case model.totem of
|
||||
NotAsked ->
|
||||
div []
|
||||
[ h1 [] [ text "Pair a new Lamassu cryptomat" ]
|
||||
, div []
|
||||
[ label []
|
||||
[ text "Cryptomat name"
|
||||
, input
|
||||
[ onInput InputName
|
||||
, placeholder "Coffee shop, 43 Elm St."
|
||||
, size 50
|
||||
, style [ ( "margin-left", "1em" ) ]
|
||||
]
|
||||
[]
|
||||
, button
|
||||
[ onClick SubmitName, disabled (String.isEmpty model.name) ]
|
||||
[ text "Pair" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Loading ->
|
||||
div []
|
||||
[ div [] [ text "..." ] ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success totem ->
|
||||
div
|
||||
[]
|
||||
[ div
|
||||
[ class [ C.QrCode ] ]
|
||||
[ qrCode totem ]
|
||||
, div []
|
||||
[ span [] [ text "Scan this QR Code to pair " ]
|
||||
, strong [] [ text model.name ]
|
||||
]
|
||||
]
|
||||
else
|
||||
div [] [ text "Make sure lamassu-server is up before pairing" ]
|
||||
|
|
@ -1,552 +0,0 @@
|
|||
module Selectize
|
||||
exposing
|
||||
( initialSelectize
|
||||
, view
|
||||
, State
|
||||
, Config
|
||||
, HtmlOptions
|
||||
, HtmlClasses
|
||||
)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (value, defaultValue, maxlength, class, classList, id, disabled)
|
||||
import Html.Events as E exposing (on, onWithOptions)
|
||||
import String
|
||||
import Json.Decode as Json
|
||||
import Css.Classes exposing (CssClasses)
|
||||
import Css.Admin exposing (className)
|
||||
|
||||
-- MODEL
|
||||
|
||||
type alias HtmlOptions =
|
||||
{ instructionsForBlank : String
|
||||
, noMatches : String
|
||||
, typeForMore : String
|
||||
, atMaxLength : String
|
||||
, noOptions : String
|
||||
, notAvailable : String
|
||||
, classes : HtmlClasses
|
||||
, customCssClass : CssClasses
|
||||
}
|
||||
|
||||
|
||||
type alias HtmlClasses =
|
||||
{ container : String
|
||||
, noOptions : String
|
||||
, singleItemContainer : String
|
||||
, multiItemContainer : String
|
||||
, selectBox : String
|
||||
, selectedItems : String
|
||||
, fallbackItems : String
|
||||
, fallbackItem : String
|
||||
, selectedItem : String
|
||||
, boxContainer : String
|
||||
, boxItems : String
|
||||
, boxItem : String
|
||||
, boxItemActive : String
|
||||
, info : String
|
||||
, infoNoMatches : String
|
||||
, inputEditing : String
|
||||
}
|
||||
|
||||
|
||||
type alias H =
|
||||
HtmlOptions
|
||||
|
||||
|
||||
type Status
|
||||
= Initial
|
||||
| Editing
|
||||
| Cleared
|
||||
| Idle
|
||||
| Blurred
|
||||
|
||||
|
||||
type alias State =
|
||||
{ boxPosition : Int
|
||||
, status : Status
|
||||
, string : String
|
||||
}
|
||||
|
||||
|
||||
type alias Config msg idType itemType =
|
||||
{ maxItems : Int
|
||||
, boxLength : Int
|
||||
, toMsg : State -> msg
|
||||
, onAdd : idType -> State -> msg
|
||||
, onRemove : State -> msg
|
||||
, onFocus : State -> msg
|
||||
, onBlur : State -> msg
|
||||
, toId : itemType -> idType
|
||||
, enabled : Bool
|
||||
, selectedDisplay : itemType -> String
|
||||
, optionDisplay : itemType -> String
|
||||
, match : String -> List itemType -> List itemType
|
||||
, htmlOptions : HtmlOptions
|
||||
}
|
||||
|
||||
|
||||
type alias Items itemType =
|
||||
{ selectedItems : List itemType
|
||||
, availableItems : List itemType
|
||||
, boxItems : List itemType
|
||||
}
|
||||
|
||||
|
||||
initialSelectize : State
|
||||
initialSelectize =
|
||||
{ boxPosition = -1, string = "", status = Blurred }
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
clean : String -> String
|
||||
clean s =
|
||||
String.trim s
|
||||
|> String.toLower
|
||||
|
||||
|
||||
updateKeyUp : Config msg idType itemType -> Items itemType -> State -> Int -> msg
|
||||
updateKeyUp config items state keyCode =
|
||||
if keyCode == 13 || keyCode == 9 then
|
||||
config.toMsg { state | status = Initial }
|
||||
else
|
||||
config.toMsg state
|
||||
|
||||
|
||||
addSelection : Config msg idType itemType -> Items itemType -> State -> msg
|
||||
addSelection config items state =
|
||||
let
|
||||
maybeItem =
|
||||
if state.boxPosition < 0 then
|
||||
Nothing
|
||||
else
|
||||
(List.head << (List.drop state.boxPosition)) items.boxItems
|
||||
in
|
||||
case maybeItem of
|
||||
Nothing ->
|
||||
config.toMsg state
|
||||
|
||||
Just item ->
|
||||
config.onAdd (config.toId item) { state | status = Cleared, string = "", boxPosition = -1 }
|
||||
|
||||
|
||||
updateKeyDown : Config msg idType itemType -> Items itemType -> State -> Int -> msg
|
||||
updateKeyDown config items state keyCode =
|
||||
if config.maxItems > 1 && List.length items.selectedItems == config.maxItems then
|
||||
case keyCode of
|
||||
-- backspace
|
||||
8 ->
|
||||
if String.isEmpty state.string && (not << List.isEmpty) items.selectedItems then
|
||||
config.onRemove state
|
||||
else
|
||||
config.toMsg state
|
||||
|
||||
_ ->
|
||||
config.toMsg state
|
||||
else
|
||||
case keyCode of
|
||||
-- up
|
||||
38 ->
|
||||
config.toMsg { state | boxPosition = (max -1 (state.boxPosition - 1)) }
|
||||
|
||||
-- down
|
||||
40 ->
|
||||
config.toMsg
|
||||
{ state
|
||||
| boxPosition =
|
||||
(min ((List.length items.boxItems) - 1)
|
||||
(state.boxPosition + 1)
|
||||
)
|
||||
}
|
||||
|
||||
-- enter
|
||||
13 ->
|
||||
addSelection config items state
|
||||
|
||||
-- backspace
|
||||
8 ->
|
||||
if String.isEmpty state.string && (not << List.isEmpty) items.selectedItems then
|
||||
config.onRemove state
|
||||
else
|
||||
config.toMsg state
|
||||
|
||||
-- tab
|
||||
9 ->
|
||||
addSelection config items state
|
||||
|
||||
_ ->
|
||||
config.toMsg state
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
itemView : Config msg idType itemType -> Bool -> itemType -> Html msg
|
||||
itemView config isFallback item =
|
||||
let
|
||||
c =
|
||||
config.htmlOptions.classes
|
||||
in
|
||||
span
|
||||
[ classList
|
||||
[ ( c.selectedItem, True )
|
||||
, ( c.fallbackItem, isFallback )
|
||||
]
|
||||
]
|
||||
[ text (config.selectedDisplay item) ]
|
||||
|
||||
|
||||
fallbackItemsView : Config msg idType itemType -> Items itemType -> List itemType -> State -> Html msg
|
||||
fallbackItemsView config items fallbackItems state =
|
||||
let
|
||||
c =
|
||||
config.htmlOptions.classes
|
||||
|
||||
selectedItems =
|
||||
items.selectedItems
|
||||
|
||||
isFallback =
|
||||
List.length selectedItems == 0
|
||||
|
||||
classes =
|
||||
classList
|
||||
[ ( c.selectedItems, True )
|
||||
, ( c.fallbackItems, isFallback )
|
||||
]
|
||||
|
||||
itemsView =
|
||||
if isFallback then
|
||||
fallbackItems
|
||||
else
|
||||
items.selectedItems
|
||||
in
|
||||
span [ classes ] (List.map (itemView config isFallback) itemsView)
|
||||
|
||||
|
||||
itemsView : Config msg idType itemType -> Items itemType -> List itemType -> State -> Html msg
|
||||
itemsView config items fallbackItems state =
|
||||
case state.status of
|
||||
Editing ->
|
||||
fallbackItemsView config items [] state
|
||||
|
||||
Initial ->
|
||||
fallbackItemsView config items [] state
|
||||
|
||||
Idle ->
|
||||
fallbackItemsView config items [] state
|
||||
|
||||
Cleared ->
|
||||
fallbackItemsView config items fallbackItems state
|
||||
|
||||
Blurred ->
|
||||
fallbackItemsView config items fallbackItems state
|
||||
|
||||
|
||||
editingBoxView : Config msg idType itemType -> Items itemType -> State -> Html msg
|
||||
editingBoxView config items state =
|
||||
let
|
||||
h =
|
||||
config.htmlOptions
|
||||
|
||||
c =
|
||||
h.classes
|
||||
|
||||
boxItemHtml pos item =
|
||||
div
|
||||
[ classList
|
||||
[ ( c.boxItem, True )
|
||||
, ( c.boxItemActive, state.boxPosition == pos )
|
||||
]
|
||||
, onMouseDown config state (config.toId item)
|
||||
]
|
||||
[ text (config.optionDisplay item)
|
||||
]
|
||||
in
|
||||
div [ class c.boxItems ] (List.indexedMap boxItemHtml items.boxItems)
|
||||
|
||||
|
||||
idleBoxView : Config msg idType itemType -> Items itemType -> State -> Html msg
|
||||
idleBoxView config items state =
|
||||
let
|
||||
h =
|
||||
config.htmlOptions
|
||||
|
||||
numSelected =
|
||||
List.length items.selectedItems
|
||||
|
||||
remainingItems =
|
||||
List.length items.availableItems - numSelected
|
||||
|
||||
typeForMore =
|
||||
if remainingItems > config.boxLength then
|
||||
if numSelected < config.maxItems then
|
||||
div [ class h.classes.info ] [ text h.typeForMore ]
|
||||
else
|
||||
div [ class h.classes.info ] [ text "Backspace for more" ]
|
||||
else
|
||||
span [] []
|
||||
in
|
||||
if config.maxItems > 1 && List.length items.selectedItems == config.maxItems then
|
||||
span [] []
|
||||
else
|
||||
div [ class h.classes.boxContainer ]
|
||||
[ editingBoxView config items state
|
||||
, typeForMore
|
||||
]
|
||||
|
||||
|
||||
noMatches : Config msg idType itemType -> List itemType -> State -> Html msg
|
||||
noMatches config boxItems state =
|
||||
let
|
||||
h =
|
||||
config.htmlOptions
|
||||
in
|
||||
if List.length boxItems == 0 then
|
||||
div
|
||||
[ classList
|
||||
[ ( h.classes.info, True )
|
||||
, ( h.classes.infoNoMatches, True )
|
||||
]
|
||||
]
|
||||
[ text h.noMatches ]
|
||||
else
|
||||
span [] []
|
||||
|
||||
|
||||
boxView : Config msg idType itemType -> Items itemType -> State -> Html msg
|
||||
boxView config items state =
|
||||
let
|
||||
h =
|
||||
config.htmlOptions
|
||||
in
|
||||
case state.status of
|
||||
Editing ->
|
||||
div [ class h.classes.boxContainer ]
|
||||
[ editingBoxView config items state
|
||||
, noMatches config items.boxItems state
|
||||
]
|
||||
|
||||
Initial ->
|
||||
idleBoxView config items state
|
||||
|
||||
Idle ->
|
||||
idleBoxView config items state
|
||||
|
||||
Cleared ->
|
||||
idleBoxView config items state
|
||||
|
||||
Blurred ->
|
||||
span [] []
|
||||
|
||||
|
||||
buildItems : List itemType -> List itemType -> List itemType -> Items itemType
|
||||
buildItems selectedItems availableItems boxItems =
|
||||
{ selectedItems = selectedItems
|
||||
, availableItems = availableItems
|
||||
, boxItems = boxItems
|
||||
}
|
||||
|
||||
|
||||
diffItems : Config msg idType itemType -> List itemType -> List itemType -> List itemType
|
||||
diffItems config a b =
|
||||
let
|
||||
isEqual itemA itemB =
|
||||
config.toId itemA == config.toId itemB
|
||||
|
||||
notInB b item =
|
||||
(List.any (isEqual item) b)
|
||||
|> not
|
||||
in
|
||||
List.filter (notInB b) a
|
||||
|
||||
|
||||
mapToItem : (itemType -> idType) -> List itemType -> idType -> Maybe itemType
|
||||
mapToItem toId available id =
|
||||
List.filter (((==) id) << toId) available
|
||||
|> List.head
|
||||
|
||||
|
||||
view : Config msg idType itemType -> List idType -> List itemType -> List idType -> State -> Html msg
|
||||
view config selectedIds availableItems fallbackIds state =
|
||||
if List.length availableItems == 0 then
|
||||
div [ class config.htmlOptions.classes.container ]
|
||||
[ div [ class config.htmlOptions.classes.noOptions ] [ text config.htmlOptions.noOptions ] ]
|
||||
else if not config.enabled then
|
||||
div [ class config.htmlOptions.classes.container ]
|
||||
[ div [ class config.htmlOptions.classes.noOptions ] [ text "\x2008" ] ]
|
||||
else
|
||||
let
|
||||
h =
|
||||
config.htmlOptions
|
||||
|
||||
selectedItems =
|
||||
List.filterMap (mapToItem config.toId availableItems) selectedIds
|
||||
|
||||
fallbackItems =
|
||||
List.filterMap (mapToItem config.toId availableItems) fallbackIds
|
||||
|
||||
remainingItems =
|
||||
diffItems config availableItems selectedItems
|
||||
|
||||
boxItems =
|
||||
config.match state.string remainingItems
|
||||
|> List.take 5
|
||||
|
||||
items =
|
||||
buildItems selectedItems availableItems boxItems
|
||||
|
||||
onInputAtt =
|
||||
onInput config state
|
||||
|
||||
onBlurAtt =
|
||||
onBlur config state
|
||||
|
||||
onFocusAtt =
|
||||
onFocus config state
|
||||
|
||||
keyDown =
|
||||
if config.maxItems > 1 then
|
||||
if String.isEmpty state.string then
|
||||
onKeyDownNoDelete config items state
|
||||
else
|
||||
onKeyDownDelete config items state
|
||||
else
|
||||
onKeyDown config items state
|
||||
|
||||
editInput =
|
||||
case state.status of
|
||||
Initial ->
|
||||
if (List.length selectedItems) < config.maxItems then
|
||||
input [ onBlurAtt, onInputAtt ] []
|
||||
else
|
||||
input [ onBlurAtt, onInputAtt, maxlength 0 ] []
|
||||
|
||||
Idle ->
|
||||
if (List.length selectedItems) < config.maxItems then
|
||||
input [ onBlurAtt, onInputAtt ] []
|
||||
else
|
||||
input [ onBlurAtt, onInputAtt, maxlength 0 ] []
|
||||
|
||||
Editing ->
|
||||
let
|
||||
actualMaxlength =
|
||||
if List.length boxItems == 0 then
|
||||
0
|
||||
else
|
||||
524288
|
||||
in
|
||||
input [ maxlength actualMaxlength, onBlurAtt, onInputAtt, class h.classes.inputEditing ] []
|
||||
|
||||
Cleared ->
|
||||
input [ onKeyUp config items state, value "", onBlurAtt, onInputAtt ] []
|
||||
|
||||
Blurred ->
|
||||
input [ maxlength 0, onFocusAtt, value "" ] []
|
||||
in
|
||||
div [ class h.classes.container ]
|
||||
[ label
|
||||
[ classList
|
||||
[ ( h.classes.singleItemContainer, config.maxItems == 1 )
|
||||
, ( h.classes.multiItemContainer, config.maxItems > 1 )
|
||||
, ( className config.htmlOptions.customCssClass, True)
|
||||
]
|
||||
]
|
||||
[ span [ class h.classes.selectBox, keyDown ]
|
||||
[ span [] [ itemsView config items fallbackItems state ]
|
||||
, editInput
|
||||
]
|
||||
, boxView config items state
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
onInput : Config msg idType itemType -> State -> Attribute msg
|
||||
onInput config state =
|
||||
let
|
||||
tagger s =
|
||||
if (String.length s == 0) then
|
||||
config.toMsg { state | status = Idle, string = s }
|
||||
else
|
||||
config.toMsg { state | status = Editing, string = s }
|
||||
in
|
||||
E.onInput tagger
|
||||
|
||||
|
||||
onMouseDown : Config msg idType itemType -> State -> idType -> Attribute msg
|
||||
onMouseDown config state id =
|
||||
E.onMouseDown (config.onAdd id state)
|
||||
|
||||
|
||||
onBlur : Config msg idType itemType -> State -> Attribute msg
|
||||
onBlur config state =
|
||||
E.onBlur (config.onBlur { state | status = Blurred })
|
||||
|
||||
|
||||
onFocus : Config msg idType itemType -> State -> Attribute msg
|
||||
onFocus config state =
|
||||
E.onFocus (config.onFocus { state | status = Initial, boxPosition = -1 })
|
||||
|
||||
|
||||
onKeyDownDelete : Config msg idType itemType -> Items itemType -> State -> Attribute msg
|
||||
onKeyDownDelete config items state =
|
||||
rawOnKeyDown deleteSpecialKeys (updateKeyDown config items state)
|
||||
|
||||
|
||||
onKeyDownNoDelete : Config msg idType itemType -> Items itemType -> State -> Attribute msg
|
||||
onKeyDownNoDelete config items state =
|
||||
rawOnKeyDown noDeleteSpecialKeys (updateKeyDown config items state)
|
||||
|
||||
|
||||
onKeyDown : Config msg idType itemType -> Items itemType -> State -> Attribute msg
|
||||
onKeyDown config items state =
|
||||
rawOnKeyDownNoPrevent (updateKeyDown config items state)
|
||||
|
||||
|
||||
onKeyUp : Config msg idType itemType -> Items itemType -> State -> Attribute msg
|
||||
onKeyUp config items state =
|
||||
rawOnKeyUp (updateKeyUp config items state)
|
||||
|
||||
|
||||
noDeleteSpecialKeys : List Int
|
||||
noDeleteSpecialKeys =
|
||||
[ 8, 38, 40, 9, 13, 27 ]
|
||||
|
||||
|
||||
deleteSpecialKeys : List Int
|
||||
deleteSpecialKeys =
|
||||
[ 38, 40, 9, 13, 27 ]
|
||||
|
||||
|
||||
preventSpecialDecoder : List Int -> Json.Decoder Int
|
||||
preventSpecialDecoder specialKeys =
|
||||
E.keyCode
|
||||
|> Json.andThen
|
||||
(\code ->
|
||||
if List.member code specialKeys then
|
||||
Json.succeed code
|
||||
else
|
||||
Json.fail "don't prevent"
|
||||
)
|
||||
|
||||
|
||||
rawOnKeyDown : List Int -> (Int -> msg) -> Attribute msg
|
||||
rawOnKeyDown specialKeys tagger =
|
||||
let
|
||||
options =
|
||||
{ stopPropagation = False, preventDefault = True }
|
||||
in
|
||||
onWithOptions "keydown" options (Json.map tagger (preventSpecialDecoder specialKeys))
|
||||
|
||||
|
||||
rawOnKeyDownNoPrevent : (Int -> msg) -> Attribute msg
|
||||
rawOnKeyDownNoPrevent tagger =
|
||||
onWithOptions "keydown" { stopPropagation = False, preventDefault = False } (Json.map tagger E.keyCode)
|
||||
|
||||
|
||||
rawOnKeyUp : (Int -> msg) -> Attribute msg
|
||||
rawOnKeyUp tagger =
|
||||
on "keyup" (Json.map tagger E.keyCode)
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
module SelectizeHelper exposing (LocalConfig, buildConfig)
|
||||
|
||||
import Selectize exposing (..)
|
||||
import Css.Selectize
|
||||
import Css.Classes exposing (CssClasses)
|
||||
|
||||
|
||||
type alias LocalConfig msg idType itemType =
|
||||
{ toMsg : State -> msg
|
||||
, onAdd : idType -> State -> msg
|
||||
, onRemove : State -> msg
|
||||
, onFocus : State -> msg
|
||||
, onBlur : State -> msg
|
||||
, toId : itemType -> idType
|
||||
, enabled : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias SpecificConfig itemType =
|
||||
{ selectedDisplay : itemType -> String
|
||||
, optionDisplay : itemType -> String
|
||||
, maxItems : Int
|
||||
, match : String -> List itemType -> List itemType
|
||||
, customCssClass: CssClasses
|
||||
}
|
||||
|
||||
|
||||
buildConfig :
|
||||
LocalConfig msg idType itemType
|
||||
-> SpecificConfig itemType
|
||||
-> Config msg idType itemType
|
||||
buildConfig localConfig specificConfig =
|
||||
{ maxItems = specificConfig.maxItems
|
||||
, boxLength = 5
|
||||
, toMsg = localConfig.toMsg
|
||||
, onAdd = localConfig.onAdd
|
||||
, onRemove = localConfig.onRemove
|
||||
, onFocus = localConfig.onFocus
|
||||
, onBlur = localConfig.onBlur
|
||||
, toId = localConfig.toId
|
||||
, enabled = localConfig.enabled
|
||||
, selectedDisplay = specificConfig.selectedDisplay
|
||||
, optionDisplay = specificConfig.optionDisplay
|
||||
, match = specificConfig.match
|
||||
, htmlOptions =
|
||||
{ instructionsForBlank = "Start typing for options"
|
||||
, noMatches = "No matches"
|
||||
, atMaxLength = "Type backspace to edit"
|
||||
, typeForMore = "Type for more options"
|
||||
, noOptions = "No options"
|
||||
, notAvailable = "N/A"
|
||||
, classes = Css.Selectize.classes
|
||||
, customCssClass = specificConfig.customCssClass
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
module StatusDecoder exposing (..)
|
||||
|
||||
import StatusTypes exposing (..)
|
||||
import Json.Decode exposing (..)
|
||||
|
||||
|
||||
rateDecoder : Decoder Rate
|
||||
rateDecoder =
|
||||
map3 Rate
|
||||
(field "crypto" string)
|
||||
(field "bid" float)
|
||||
(field "ask" float)
|
||||
|
||||
|
||||
serverDecoder : Decoder ServerRec
|
||||
serverDecoder =
|
||||
map5 ServerRec
|
||||
(field "up" bool)
|
||||
(field "lastPing" (nullable string))
|
||||
(field "rates" (list rateDecoder))
|
||||
(field "machineStatus" string)
|
||||
(field "wasConfigured" bool)
|
||||
|
||||
|
||||
statusDecoder : Decoder StatusRec
|
||||
statusDecoder =
|
||||
map2 StatusRec
|
||||
(field "server" serverDecoder)
|
||||
(field "invalidConfigGroups" (list string))
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
module StatusTypes exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
|
||||
|
||||
type alias Rate =
|
||||
{ crypto : String
|
||||
, bid : Float
|
||||
, ask : Float
|
||||
}
|
||||
|
||||
|
||||
type alias ServerRec =
|
||||
{ up : Bool
|
||||
, lastPing : Maybe String
|
||||
, rates : List Rate
|
||||
, machineStatus : String
|
||||
, wasConfigured : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias StatusRec =
|
||||
{ server : ServerRec
|
||||
, invalidConfigGroups : List String
|
||||
}
|
||||
|
||||
|
||||
type alias WebStatus =
|
||||
WebData StatusRec
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
port module Stylesheets exposing (..)
|
||||
|
||||
import Css.File exposing (CssFileStructure, CssCompilerProgram)
|
||||
import Css.Main
|
||||
|
||||
|
||||
port files : CssFileStructure -> Cmd msg
|
||||
|
||||
|
||||
fileStructure : CssFileStructure
|
||||
fileStructure =
|
||||
Css.File.toFileStructure
|
||||
[ ( "../public/styles.css", Css.File.compile [ Css.Main.css ] ) ]
|
||||
|
||||
|
||||
main : CssCompilerProgram
|
||||
main =
|
||||
Css.File.compiler files fileStructure
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
module SupportLogs.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import Common.Logs.Decoder exposing (logsDecoder, supportLogDecoder, supportLogsDecoder)
|
||||
import SupportLogs.Types exposing (..)
|
||||
|
||||
|
||||
getAllLogs : Maybe String -> Cmd Msg
|
||||
getAllLogs maybeId =
|
||||
Http.get ("/api/support_logs/logs?supportLogId=" ++ (Maybe.withDefault "" maybeId)) logsDecoder
|
||||
|> RemoteData.sendRequest
|
||||
|> Cmd.map LoadLogs
|
||||
|
||||
|
||||
getSupportLogs : Cmd Msg
|
||||
getSupportLogs =
|
||||
Http.get "/api/support_logs/" supportLogsDecoder
|
||||
|> RemoteData.sendRequest
|
||||
|> Cmd.map LoadSupportLogs
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
module SupportLogs.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import SupportLogs.Rest exposing (..)
|
||||
import SupportLogs.Types exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ logs = NotAsked, supportLogs = NotAsked }
|
||||
|
||||
|
||||
load : Maybe String -> ( Model, Cmd Msg )
|
||||
load maybeId =
|
||||
( { logs = Loading, supportLogs = Loading }, getSupportData maybeId )
|
||||
|
||||
|
||||
getSupportData : Maybe String -> Cmd Msg
|
||||
getSupportData maybeId =
|
||||
Cmd.batch [ getAllLogs maybeId, getSupportLogs ]
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
LoadLogs response ->
|
||||
( { model | logs = response }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
LoadSupportLogs response ->
|
||||
( { model | supportLogs = response }
|
||||
, Cmd.none
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
module SupportLogs.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Common.Logs.Types exposing (..)
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ logs : WebData Logs
|
||||
, supportLogs : WebData SupportLogs
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= LoadLogs (WebData Logs)
|
||||
| LoadSupportLogs (WebData SupportLogs)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
module SupportLogs.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (href)
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import RemoteData exposing (..)
|
||||
import List
|
||||
import Common.Logs.Types exposing (..)
|
||||
import SupportLogs.Types exposing (..)
|
||||
import Date exposing (..)
|
||||
import Date.Extra exposing (toFormattedString)
|
||||
|
||||
|
||||
supportLogText : SupportLog -> Html Msg
|
||||
supportLogText supportLog =
|
||||
text (supportLog.name ++ " " ++ (toFormattedString "yyyy-MM-dd HH:mm" supportLog.timestamp))
|
||||
|
||||
|
||||
supportLogLink : SupportLog -> Html Msg
|
||||
supportLogLink supportLog =
|
||||
a [ href ("/#support_logs/" ++ supportLog.id) ] [ supportLogText supportLog ]
|
||||
|
||||
|
||||
formatDate : Date -> String
|
||||
formatDate date =
|
||||
toFormattedString "yyyy-MM-dd HH:mm" date
|
||||
|
||||
|
||||
rowView : Log -> Html Msg
|
||||
rowView log =
|
||||
tr [ class [] ]
|
||||
[ td [] [ text (formatDate log.timestamp) ]
|
||||
, td [] [ text log.logLevel ]
|
||||
, td [] [ text log.message ]
|
||||
]
|
||||
|
||||
|
||||
supportLogItemView : SupportLog -> Html Msg
|
||||
supportLogItemView supportLog =
|
||||
li [] [ supportLogLink supportLog ]
|
||||
|
||||
|
||||
supportLogsView : SupportLogs -> Html Msg
|
||||
supportLogsView supportLogs =
|
||||
if List.isEmpty supportLogs then
|
||||
div [ class [ C.EmptyTable ] ] [ text "No shared logs" ]
|
||||
else
|
||||
div []
|
||||
[ div [ class [ C.TxTable ] ]
|
||||
[ ul [] (List.map supportLogItemView supportLogs)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
supportLogs : Model -> Html Msg
|
||||
supportLogs model =
|
||||
case model.supportLogs of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading snapshots ..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success supportLogs ->
|
||||
div [] [ supportLogsView supportLogs ]
|
||||
|
||||
|
||||
logsView : Logs -> Html Msg
|
||||
logsView logs =
|
||||
if List.isEmpty logs.logs then
|
||||
div [] [ text "No logs yet." ]
|
||||
else
|
||||
div []
|
||||
[ table [ class [ C.TxTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [] [ text "Date" ]
|
||||
, td [] [ text "Level" ]
|
||||
, td [] [ text "Message" ]
|
||||
]
|
||||
]
|
||||
, tbody [] (List.map rowView logs.logs)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
logs : Model -> Html Msg
|
||||
logs model =
|
||||
case model.logs of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading logs..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text "No logs yet." ]
|
||||
|
||||
Success logs ->
|
||||
div []
|
||||
[ logsView logs
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div []
|
||||
[ h1 [] [ text "Lamassu support logs" ]
|
||||
, div [ class [ C.PaneWrapper ] ]
|
||||
[ div [ class [ C.LeftPane ] ]
|
||||
[ h2 [] [ text "Shared snapshots" ]
|
||||
, supportLogs model
|
||||
]
|
||||
, div [ class [ C.ContentPane ] ]
|
||||
[ h2 [] [ text "Logs" ]
|
||||
, logs model
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
module Transaction.Decoder exposing (..)
|
||||
|
||||
import Json.Decode exposing (..)
|
||||
import Json.Decode.Extra exposing (date, fromResult)
|
||||
import Json.Decode.Pipeline exposing (decode, required, optional, hardcoded)
|
||||
import Common.TransactionTypes exposing (..)
|
||||
import String
|
||||
|
||||
mapCryptoCode : String -> Decoder CryptoCode
|
||||
mapCryptoCode code =
|
||||
case code of
|
||||
"BTC" -> succeed BTC
|
||||
"BCH" -> succeed BCH
|
||||
"ETH" -> succeed ETH
|
||||
"ZEC" -> succeed ZEC
|
||||
"DASH" -> succeed DASH
|
||||
"LTC" -> succeed LTC
|
||||
_ -> fail ("No such cryptocurrency: " ++ code)
|
||||
|
||||
cryptoCodeDecoder : Decoder CryptoCode
|
||||
cryptoCodeDecoder =
|
||||
string
|
||||
|> andThen mapCryptoCode
|
||||
|
||||
txDecode : String -> Decoder Tx
|
||||
txDecode txClass =
|
||||
case txClass of
|
||||
"cashIn" ->
|
||||
map CashInTx cashInTxDecoder
|
||||
|
||||
"cashOut" ->
|
||||
map CashOutTx cashOutTxDecoder
|
||||
|
||||
_ ->
|
||||
fail ("Unknown tx class: " ++ txClass)
|
||||
|
||||
|
||||
txsDecoder : Decoder (List Tx)
|
||||
txsDecoder =
|
||||
(field "transactions" (list txDecoder))
|
||||
|
||||
|
||||
txDecoder : Decoder Tx
|
||||
txDecoder =
|
||||
(field "txClass" string)
|
||||
|> andThen txDecode
|
||||
|
||||
|
||||
floatString : Decoder Float
|
||||
floatString =
|
||||
string |> andThen (String.toFloat >> fromResult)
|
||||
|
||||
|
||||
intString : Decoder Int
|
||||
intString =
|
||||
string |> andThen (String.toInt >> fromResult)
|
||||
|
||||
|
||||
cashInTxDecoder : Decoder CashInTxRec
|
||||
cashInTxDecoder =
|
||||
decode CashInTxRec
|
||||
|> required "id" string
|
||||
|> required "machineName" string
|
||||
|> required "toAddress" string
|
||||
|> required "cryptoAtoms" intString
|
||||
|> required "cryptoCode" cryptoCodeDecoder
|
||||
|> required "fiat" floatString
|
||||
|> required "commissionPercentage" (nullable floatString)
|
||||
|> required "rawTickerPrice" (nullable floatString)
|
||||
|> required "fiatCode" string
|
||||
|> required "txHash" (nullable string)
|
||||
|> required "phone" (nullable string)
|
||||
|> required "error" (nullable string)
|
||||
|> required "operatorCompleted" bool
|
||||
|> required "send" bool
|
||||
|> required "sendConfirmed" bool
|
||||
|> required "expired" bool
|
||||
|> required "created" date
|
||||
|
||||
|
||||
confirmedDecoder : Decoder Bool
|
||||
confirmedDecoder =
|
||||
map (Maybe.map (always True) >> Maybe.withDefault False)
|
||||
(nullable string)
|
||||
|
||||
|
||||
cashOutTxDecoder : Decoder CashOutTxRec
|
||||
cashOutTxDecoder =
|
||||
decode CashOutTxRec
|
||||
|> required "id" string
|
||||
|> required "machineName" string
|
||||
|> required "toAddress" string
|
||||
|> required "cryptoAtoms" intString
|
||||
|> required "cryptoCode" cryptoCodeDecoder
|
||||
|> required "fiat" floatString
|
||||
|> required "commissionPercentage" (nullable floatString)
|
||||
|> required "rawTickerPrice" (nullable floatString)
|
||||
|> required "fiatCode" string
|
||||
|> required "status" string
|
||||
|> required "dispense" bool
|
||||
|> required "notified" bool
|
||||
|> required "redeem" bool
|
||||
|> required "phone" (nullable string)
|
||||
|> required "error" (nullable string)
|
||||
|> required "created" date
|
||||
|> required "confirmedAt" confirmedDecoder
|
||||
|> required "expired" bool
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
module Transaction.Rest exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import HttpBuilder exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
import Common.TransactionTypes exposing (..)
|
||||
import Transaction.Types exposing (..)
|
||||
import Transaction.Decoder exposing (txDecoder)
|
||||
|
||||
|
||||
toModel : SavingStatus -> Tx -> SubModel
|
||||
toModel status tx =
|
||||
{ status = status, tx = tx }
|
||||
|
||||
|
||||
getForm : String -> Cmd Msg
|
||||
getForm txId =
|
||||
get ("/api/transaction/" ++ txId)
|
||||
|> withExpect (Http.expectJson txDecoder)
|
||||
|> send (Result.map (toModel NotSaving) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
cancel : String -> Cmd Msg
|
||||
cancel txId =
|
||||
patch ("/api/transaction/" ++ txId ++ "?cancel=true")
|
||||
|> withExpect (Http.expectJson txDecoder)
|
||||
|> send (Result.map (toModel NotSaving) >> RemoteData.fromResult)
|
||||
|> Cmd.map Load
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
module Transaction.State exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import Transaction.Types exposing (..)
|
||||
import Transaction.Rest exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
load : String -> ( Model, Cmd Msg )
|
||||
load txId =
|
||||
( Loading, getForm txId )
|
||||
|
||||
|
||||
txUpdate : SubModel -> ( SubModel, Cmd Msg )
|
||||
txUpdate model =
|
||||
model ! []
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load newModel ->
|
||||
RemoteData.update txUpdate newModel
|
||||
|
||||
Cancel txId ->
|
||||
model ! [ cancel txId ]
|
||||
|
||||
HideSaveIndication ->
|
||||
RemoteData.update (\subModel -> { subModel | status = NotSaving } ! []) model
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
module Transaction.Types exposing (..)
|
||||
|
||||
import RemoteData exposing (..)
|
||||
import BasicTypes exposing (..)
|
||||
import Common.TransactionTypes exposing (..)
|
||||
|
||||
|
||||
type alias SubModel =
|
||||
{ status : SavingStatus
|
||||
, tx : Tx
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData SubModel
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
| Cancel String
|
||||
| HideSaveIndication
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
module Transaction.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Events exposing (onClick)
|
||||
import RemoteData exposing (..)
|
||||
import Common.TransactionTypes exposing (..)
|
||||
import Transaction.Types exposing (..)
|
||||
import Numeral exposing (format)
|
||||
import Maybe.Extra exposing (isJust)
|
||||
|
||||
|
||||
-- import Css.Admin exposing (..)
|
||||
-- import Css.Classes as C
|
||||
|
||||
|
||||
cashInTxView : CashInTxRec -> Html Msg
|
||||
cashInTxView tx =
|
||||
let
|
||||
cancelStatus =
|
||||
if tx.operatorCompleted then
|
||||
"Cancelled"
|
||||
else if tx.sendConfirmed then
|
||||
"Sent"
|
||||
else if tx.expired then
|
||||
"Expired"
|
||||
else
|
||||
"Pending"
|
||||
|
||||
cancellable =
|
||||
not (tx.operatorCompleted || tx.sendConfirmed || tx.expired)
|
||||
|
||||
cancelButtonDiv =
|
||||
if cancellable then
|
||||
div []
|
||||
[ button [ onClick (Cancel tx.id) ] [ text "Cancel transaction" ]
|
||||
]
|
||||
else
|
||||
div [] []
|
||||
|
||||
error =
|
||||
Maybe.withDefault "Successful" tx.error
|
||||
in
|
||||
div []
|
||||
[ div [] [ text tx.id ]
|
||||
, div [] [ text "This is a cash-in transaction" ]
|
||||
, div [] [ text ("Fiat: " ++ (format "0,0.00" tx.fiat)) ]
|
||||
, div [] [ text ("Raw ticker price: " ++ (format "0,0.00" (Maybe.withDefault 0.0 tx.rawTickerPrice))) ]
|
||||
, div [] [ text ("Status: " ++ cancelStatus) ]
|
||||
, div [] [ text error ]
|
||||
, cancelButtonDiv
|
||||
]
|
||||
|
||||
|
||||
cashOutTxView : CashOutTxRec -> Html Msg
|
||||
cashOutTxView tx =
|
||||
let
|
||||
cancelStatus =
|
||||
if isJust tx.error then
|
||||
"Error"
|
||||
else if tx.dispense then
|
||||
"Success"
|
||||
else if tx.expired then
|
||||
"Expired"
|
||||
else
|
||||
"Pending"
|
||||
error =
|
||||
case tx.error of
|
||||
Nothing ->
|
||||
"No errors"
|
||||
|
||||
Just err ->
|
||||
"Error: " ++ err
|
||||
in
|
||||
div []
|
||||
[ div [] [ text tx.id ]
|
||||
, div [] [ text "This is a cash-out transaction" ]
|
||||
, div [] [ text ("Fiat: " ++ (format "0,0.00" tx.fiat)) ]
|
||||
, div [] [ text ("Raw ticker price: " ++ (format "0,0.00" (Maybe.withDefault 0.0 tx.rawTickerPrice))) ]
|
||||
, div [] [ text ("Status: " ++ cancelStatus) ]
|
||||
, div [] [ text error ]
|
||||
]
|
||||
|
||||
|
||||
txView : SubModel -> Html Msg
|
||||
txView subModel =
|
||||
case subModel.tx of
|
||||
CashInTx cashInTxRec ->
|
||||
cashInTxView cashInTxRec
|
||||
|
||||
CashOutTx cashOutTxRec ->
|
||||
cashOutTxView cashOutTxRec
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success subModel ->
|
||||
txView subModel
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
module Transactions exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (colspan, href)
|
||||
import Css.Admin exposing (..)
|
||||
import Css.Classes as C
|
||||
import RemoteData exposing (..)
|
||||
import Http
|
||||
import HttpBuilder exposing (..)
|
||||
import Transaction.Decoder exposing (txsDecoder)
|
||||
import Common.TransactionTypes exposing (..)
|
||||
import List
|
||||
import Numeral exposing (format)
|
||||
import Date.Extra exposing (toFormattedString)
|
||||
import Maybe.Extra exposing (isJust)
|
||||
|
||||
|
||||
type alias Txs =
|
||||
List Tx
|
||||
|
||||
|
||||
type alias Model =
|
||||
RemoteData.WebData Txs
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
NotAsked
|
||||
|
||||
|
||||
loadCmd : Cmd Msg
|
||||
loadCmd =
|
||||
getTransactions
|
||||
|
||||
|
||||
load : ( Model, Cmd Msg )
|
||||
load =
|
||||
( Loading, loadCmd )
|
||||
|
||||
|
||||
getTransactions : Cmd Msg
|
||||
getTransactions =
|
||||
get ("/api/transactions")
|
||||
|> withExpect (Http.expectJson txsDecoder)
|
||||
|> send RemoteData.fromResult
|
||||
|> Cmd.map Load
|
||||
|
||||
|
||||
type Msg
|
||||
= Load Model
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
Load loadedModel ->
|
||||
loadedModel ! []
|
||||
|
||||
|
||||
multiplier : CryptoCode -> Float
|
||||
multiplier code =
|
||||
case code of
|
||||
BTC ->
|
||||
1.0e8
|
||||
|
||||
BCH ->
|
||||
1.0e8
|
||||
|
||||
ETH ->
|
||||
1.0e18
|
||||
|
||||
ZEC ->
|
||||
1.0e8
|
||||
|
||||
DASH ->
|
||||
1.0e8
|
||||
|
||||
LTC ->
|
||||
1.0e8
|
||||
|
||||
|
||||
|
||||
txLink : String -> Html Msg
|
||||
txLink txId =
|
||||
a [ href ("/#transaction/" ++ txId) ] [ text (String.left 8 txId) ]
|
||||
|
||||
|
||||
cryptoCodeDisplay : CryptoCode -> Html Msg
|
||||
cryptoCodeDisplay code =
|
||||
let
|
||||
txt : String
|
||||
txt = case code of
|
||||
BTC -> "BTC"
|
||||
BCH -> "BCH"
|
||||
ETH -> "ETH"
|
||||
ZEC -> "ZEC"
|
||||
DASH -> "DASH"
|
||||
LTC -> "LTC"
|
||||
in
|
||||
text txt
|
||||
|
||||
rowView : Tx -> Html Msg
|
||||
rowView tx =
|
||||
case tx of
|
||||
CashInTx cashIn ->
|
||||
let
|
||||
rowClasses =
|
||||
if cashIn.operatorCompleted then
|
||||
[ C.CashIn, C.TxCancelled ]
|
||||
else
|
||||
[ C.CashIn ]
|
||||
|
||||
status =
|
||||
if cashIn.operatorCompleted then
|
||||
"Cancelled"
|
||||
else if isJust cashIn.error then
|
||||
"Error"
|
||||
else if cashIn.sendConfirmed then
|
||||
"Sent"
|
||||
else if cashIn.expired then
|
||||
"Expired"
|
||||
else
|
||||
"Pending"
|
||||
in
|
||||
tr [ class rowClasses ]
|
||||
[ td [] [ txLink cashIn.id ]
|
||||
, td [] [ text status ]
|
||||
, td
|
||||
[ class [ C.NumberColumn ] ]
|
||||
[ text (toFormattedString "yyyy-MM-dd HH:mm" cashIn.created) ]
|
||||
, td [] [ text cashIn.machineName ]
|
||||
, td [ class [ C.NumberColumn ] ]
|
||||
[ text (format "0,0.000000" ((toFloat cashIn.cryptoAtoms) / multiplier cashIn.cryptoCode))
|
||||
]
|
||||
, td [] [ cryptoCodeDisplay cashIn.cryptoCode ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (format "0,0.00" cashIn.fiat) ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (format "0,0.00" (Maybe.withDefault 0.0 cashIn.commissionPercentage)) ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (Maybe.withDefault "" cashIn.phone) ]
|
||||
, td [ class [ C.TxAddress ] ] [ text cashIn.toAddress ]
|
||||
]
|
||||
|
||||
CashOutTx cashOut ->
|
||||
let
|
||||
status =
|
||||
if isJust cashOut.error then
|
||||
"Error"
|
||||
else if cashOut.dispense then
|
||||
"Success"
|
||||
else if cashOut.expired then
|
||||
"Expired"
|
||||
else
|
||||
"Pending"
|
||||
in
|
||||
tr [ class [ C.CashOut ] ]
|
||||
[ td [] [ txLink cashOut.id ]
|
||||
, td [] [ text status ]
|
||||
, td [ class [ C.NumberColumn, C.DateColumn ] ] [ text (toFormattedString "yyyy-MM-dd HH:mm" cashOut.created) ]
|
||||
, td [] [ text cashOut.machineName ]
|
||||
, td [ class [ C.NumberColumn ] ]
|
||||
[ text (format "0,0.000000" ((toFloat cashOut.cryptoAtoms) / multiplier cashOut.cryptoCode))
|
||||
]
|
||||
, td [] [ cryptoCodeDisplay cashOut.cryptoCode ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (format "0,0.00" cashOut.fiat) ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (format "0,0.00" (Maybe.withDefault 0.0 cashOut.commissionPercentage)) ]
|
||||
, td [ class [ C.NumberColumn ] ] [ text (Maybe.withDefault "" cashOut.phone) ]
|
||||
, td [ class [ C.TxAddress ] ] [ text cashOut.toAddress ]
|
||||
]
|
||||
|
||||
|
||||
tableView : Txs -> Html Msg
|
||||
tableView txs =
|
||||
if List.isEmpty txs then
|
||||
div [] [ text "No activity yet." ]
|
||||
else
|
||||
table [ class [ C.TxTable ] ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ td [ class [ C.TxId ] ] [ text "Id" ]
|
||||
, td [] [ text "Status" ]
|
||||
, td [ class [ C.TxDate ] ] [ text "Time" ]
|
||||
, td [ class [ C.TxMachine ] ] [ text "Machine" ]
|
||||
, td [ colspan 2 ] [ text "Crypto" ]
|
||||
, td [ class [ C.TxAmount ] ] [ text "Fiat" ]
|
||||
, td [ class [ C.TxAmount ] ] [ text "Commission" ]
|
||||
, td [ class [ C.TxPhone ] ] [ text "Phone" ]
|
||||
, td [ class [ C.TxAddress ] ] [ text "To address" ]
|
||||
]
|
||||
]
|
||||
, tbody [] (List.map rowView txs)
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model of
|
||||
NotAsked ->
|
||||
div [] []
|
||||
|
||||
Loading ->
|
||||
div [] [ text "Loading..." ]
|
||||
|
||||
Failure err ->
|
||||
div [] [ text (toString err) ]
|
||||
|
||||
Success txs ->
|
||||
div [] [ tableView txs ]
|
||||
1
lamassu-admin-elm/tests/.gitignore
vendored
1
lamassu-admin-elm/tests/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/elm-stuff/
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
module AccountTypesTests exposing (..)
|
||||
|
||||
import Test exposing (..)
|
||||
import Expect
|
||||
import AccountTypes
|
||||
import Result
|
||||
import FieldSetTypes exposing (..)
|
||||
import AccountDecoder
|
||||
|
||||
|
||||
testString : String
|
||||
testString =
|
||||
"""
|
||||
{
|
||||
"code": "twilio",
|
||||
"display": "Twilio",
|
||||
"fieldSet": {
|
||||
"fields": [
|
||||
{
|
||||
"code": "accountSid",
|
||||
"display": "Account SID",
|
||||
"type": "string",
|
||||
"secret": false,
|
||||
"required": true,
|
||||
"value": {
|
||||
"fieldType": "string",
|
||||
"value": "xx123"
|
||||
},
|
||||
"status": { "code": "idle" }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
testRecord : AccountTypes.Account
|
||||
testRecord =
|
||||
{ code = "twilio"
|
||||
, display = "Twilio"
|
||||
, fieldSet =
|
||||
{ fields =
|
||||
[ { code = "accountSid"
|
||||
, display = "Account SID"
|
||||
, secret = False
|
||||
, required = True
|
||||
, value = FieldString "xx123"
|
||||
, loadedValue = FieldString "xx123"
|
||||
, status = FieldIdle
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
all : Test
|
||||
all =
|
||||
describe "Parse InitialRecord"
|
||||
[ test "Basic record" <|
|
||||
\() ->
|
||||
let
|
||||
parsed =
|
||||
AccountDecoder.decodeAccount testString
|
||||
in
|
||||
Expect.equal parsed (Ok testRecord)
|
||||
]
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
module ConfigTypesTests exposing (..)
|
||||
|
||||
import Test exposing (..)
|
||||
import Expect
|
||||
import AccountTypes
|
||||
import Result
|
||||
import FieldSetTypes exposing (..)
|
||||
import ConfigTypes exposing (..)
|
||||
import ConfigDecoder exposing (configGroupDecoder)
|
||||
import Json.Decode exposing (decodeString)
|
||||
|
||||
|
||||
testString : String
|
||||
testString =
|
||||
"""
|
||||
{
|
||||
"code": "main",
|
||||
"display": "Main",
|
||||
"crypto": "global",
|
||||
"cryptoConfigs": [
|
||||
{
|
||||
"crypto": "BTC",
|
||||
"machineConfigs": [
|
||||
{
|
||||
"machine": "01",
|
||||
"fieldSet": {
|
||||
"fields": [
|
||||
{
|
||||
"code": "cash-in-commission",
|
||||
"display": "Cash In Commission",
|
||||
"secret": false,
|
||||
"required": false,
|
||||
"value": {
|
||||
"fieldType": "percentage",
|
||||
"value": 15
|
||||
},
|
||||
"status": {
|
||||
"code": "idle"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"cryptos": [
|
||||
{
|
||||
"crypto": "BTC",
|
||||
"display": "Bitcoin"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
testRecord : ConfigTypes.ConfigGroup
|
||||
testRecord =
|
||||
{ code = "main"
|
||||
, display = "Main"
|
||||
, crypto = GlobalCrypto
|
||||
, cryptoConfigs =
|
||||
[ { crypto = CryptoCode "BTC"
|
||||
, machineConfigs =
|
||||
[ { machine = MachineId "01"
|
||||
, fieldSet =
|
||||
{ fields =
|
||||
[ { code = "cash-in-commission"
|
||||
, display = "Cash In Commission"
|
||||
, secret = False
|
||||
, required = False
|
||||
, value = FieldPercentage 15
|
||||
, loadedValue = FieldPercentage 15
|
||||
, status = FieldIdle
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
, cryptos =
|
||||
[ { crypto = CryptoCode "BTC"
|
||||
, display = "Bitcoin"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
all : Test
|
||||
all =
|
||||
describe "Parse InitialRecord"
|
||||
[ test "Basic record" <|
|
||||
\() ->
|
||||
let
|
||||
parsed =
|
||||
decodeString configGroupDecoder testString
|
||||
in
|
||||
Expect.equal parsed (Ok testRecord)
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue