Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0172c86958 |
@@ -1,67 +0,0 @@
|
|||||||
name: Build and Package JRuby Rails App
|
|
||||||
|
|
||||||
#on:
|
|
||||||
# push:
|
|
||||||
# branches:
|
|
||||||
# - main
|
|
||||||
# - second_try
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Java and JRuby
|
|
||||||
run: |
|
|
||||||
apt-get update && apt-get install -y wget curl libgmp-dev
|
|
||||||
# Install Java (needed for JRuby and jpackage)
|
|
||||||
apt-get install -y openjdk-17-jdk
|
|
||||||
|
|
||||||
# Download and install JRuby directly
|
|
||||||
JRUBY_VERSION="9.4.6.0"
|
|
||||||
curl -O https://repo1.maven.org/maven2/org/jruby/jruby-dist/${JRUBY_VERSION}/jruby-dist-${JRUBY_VERSION}-bin.tar.gz
|
|
||||||
tar -xzf jruby-dist-${JRUBY_VERSION}-bin.tar.gz -C /opt/
|
|
||||||
ln -s /opt/jruby-${JRUBY_VERSION}/bin/jruby /usr/local/bin/jruby
|
|
||||||
ln -s /opt/jruby-${JRUBY_VERSION}/bin/bundle /usr/local/bin/bundle
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: |
|
|
||||||
bundle install
|
|
||||||
|
|
||||||
- name: Build JAR with Jarbler
|
|
||||||
run: |
|
|
||||||
gem install jarbler
|
|
||||||
jarble
|
|
||||||
|
|
||||||
- name: Generate Unique App Name with Git Hash
|
|
||||||
id: vars
|
|
||||||
run: |
|
|
||||||
SHORT_HASH=$(git rev-parse --short HEAD)
|
|
||||||
echo "app_name=custom-app-${SHORT_HASH}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Package with jpackage
|
|
||||||
run: |
|
|
||||||
# Find the generated jar file name dynamically
|
|
||||||
JAR_FILE=$(ls build/*.jar | head -n 1)
|
|
||||||
|
|
||||||
# Run jpackage to bundle the application and runtime into an app-image directory
|
|
||||||
jpackage \
|
|
||||||
--type app-image \
|
|
||||||
--name "${{ steps.vars.outputs.app_name }}" \
|
|
||||||
--input "$(dirname "$JAR_FILE")" \
|
|
||||||
--main-jar "$(basename "$JAR_FILE")" \
|
|
||||||
--dest dist
|
|
||||||
|
|
||||||
- name: Compress Executable Package
|
|
||||||
run: |
|
|
||||||
cd dist
|
|
||||||
tar -czvf "${{ steps.vars.outputs.app_name }}.tar.gz" "${{ steps.vars.outputs.app_name }}"
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ steps.vars.outputs.app_name }}
|
|
||||||
path: dist/*.tar.gz
|
|
||||||
@@ -3,7 +3,7 @@ name: CI
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [ main, second_try ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scan_ruby:
|
scan_ruby:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: apt-get update && apt-get install --no-install-recommends -y libvips
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
steps:
|
steps:
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: apt-get update && apt-get install --no-install-recommends -y libvips
|
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# Setting up the application
|
|
||||||
|
|
||||||
## Create the database connections:
|
|
||||||
|
|
||||||
bin/rails generate model Joints --no-migration
|
|
||||||
bin/rails generate model JointsInfo --no-migration
|
|
||||||
bin/rails generate model SpecialTest --no-migration
|
|
||||||
|
|
||||||
|
|
||||||
# Setup the views
|
|
||||||
|
|
||||||
bin/rails generate controller dashboard
|
|
||||||
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
require 'yaml'
|
|
||||||
|
|
||||||
|
|
||||||
## Debugging:
|
|
||||||
# If the response should be filtered and it is not being filtered **and** you
|
|
||||||
# are geting all the values for that column, check to make sure that you are
|
|
||||||
# selecting from the allowed tables and columns.
|
|
||||||
|
|
||||||
|
|
||||||
ALLOWED_TABLES = {
|
|
||||||
"JointInfo" => ["Joint", "Osteokinematics"],
|
|
||||||
"Joint" => ["Common Name", "Joint"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DashboardController < ApplicationController
|
|
||||||
before_action :load_manifest
|
|
||||||
|
|
||||||
def index
|
|
||||||
# Renders the main page
|
|
||||||
respond_to do |format|
|
|
||||||
format.html
|
|
||||||
format.json { render json: @manifest }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# GET /dashboard/options?table=Joint&filter_col=joint_id&value=1
|
|
||||||
def options
|
|
||||||
# 1. Define what is allowed to be accessed
|
|
||||||
|
|
||||||
table_name = params[:table]
|
|
||||||
col_name = params[:pluck_column]
|
|
||||||
|
|
||||||
# 2. Validate that the table and column exist in your allowlist
|
|
||||||
if ALLOWED_TABLES.key?(table_name) && ALLOWED_TABLES[table_name].include?(col_name)
|
|
||||||
table = table_name.constantize
|
|
||||||
quoted_col = ActiveRecord::Base.connection.quote_column_name(col_name)
|
|
||||||
|
|
||||||
if params[:filter_column].present? && ALLOWED_TABLES[table_name].include?(params[:filter_column])
|
|
||||||
puts "Filtering with 'filter_column'"
|
|
||||||
options = table.select(Arel.sql(quoted_col)).where(params[:filter_column] => params[:value]).pluck(Arel.sql(quoted_col))
|
|
||||||
else
|
|
||||||
puts "***NO FILTERING***"
|
|
||||||
options = table.distinct.pluck(Arel.sql(quoted_col))
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: options
|
|
||||||
else
|
|
||||||
render json: { error: "Invalid table or column requested" }, status: :bad_request
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# GET /dashboard/data?joint=Acromioclavicular&sub_joint=X
|
|
||||||
def data
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
# Use the data_sources from YAML to aggregate data
|
|
||||||
@manifest['data_sources'].each do |source|
|
|
||||||
debugger
|
|
||||||
table = source['table'].constantize
|
|
||||||
# Find the filter value from the params that matches the filter_column
|
|
||||||
# We assume the filter_column name in DB matches the filter ID in YAML
|
|
||||||
filter_val = params[source['filter_column']]
|
|
||||||
|
|
||||||
record = table.find_by(source['filter_column'] => filter_val)
|
|
||||||
results[source['key']] = record ? record.attributes : {}
|
|
||||||
end
|
|
||||||
|
|
||||||
#debugger
|
|
||||||
render json: results
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def load_manifest
|
|
||||||
@manifest = YAML.load_file(Rails.root.join('config', 'dashboard_manifest.yml'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
class JointsController < ApplicationController
|
||||||
|
def index
|
||||||
|
# Grab values:
|
||||||
|
@available_categories = JointsDashboard.distinct.pluck(:Joint)
|
||||||
|
|
||||||
|
# Start with base query:
|
||||||
|
@records = JointsDashboard.all
|
||||||
|
|
||||||
|
# Filter data based on dropdown selection params
|
||||||
|
@records = @records.where(Joint: params[:joint]) if params[:joint].present?
|
||||||
|
#@records = @records.where(Joint == params[:joint]) if params[:joint].present?
|
||||||
|
puts @records
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
module DashboardHelper
|
|
||||||
end
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class Joint < ApplicationRecord
|
|
||||||
def readonly?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class JointInfo < ApplicationRecord
|
|
||||||
def readonly?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class JointsDashboard < ApplicationRecord
|
||||||
|
self.table_name = "Joints"
|
||||||
|
|
||||||
|
# Prevent actidental updates from code:
|
||||||
|
def readonly?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class SpecialTest < ApplicationRecord
|
|
||||||
def readonly?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
<h1 style="font-family: Arial, sans-serif;">Dynamic Dashboard</h1>
|
|
||||||
|
|
||||||
<div id="filter-area" style="margin-bottom: 20px; display: flex; gap: 15px; font-family: Arial, sans-serif;">
|
|
||||||
<!-- Dropdowns injected here by JS -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="results-area" style="display: flex; gap: 20px;">
|
|
||||||
<!-- Grids injected here by JS -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.dyn-grid { border-collapse: collapse; border: 2px solid #000; font-family: Arial; font-size: 12px; }
|
|
||||||
.dyn-grid td { border: 1px solid #000; padding: 4px 8px; text-align: center; }
|
|
||||||
.bg-dark-blue { background-color: #002060; color: white; font-weight: bold; }
|
|
||||||
.bg-light-blue { background-color: #D9E1F2; font-weight: bold; }
|
|
||||||
.bg-pale-yellow { background-color: #FFFFCC; }
|
|
||||||
.bg-pale-green { background-color: #E2EFDA; }
|
|
||||||
.bg-pale-orange { background-color: #FCE4D6; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
let manifest = {};
|
|
||||||
let currentSelections = {};
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const resp = await fetch(
|
|
||||||
'/dashboard',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
manifest = await resp.json();
|
|
||||||
await renderRootFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderRootFilters() {
|
|
||||||
const rootFilters = manifest.filters.filter(f => f.depends_on === null);
|
|
||||||
for (const f of rootFilters) {
|
|
||||||
await createDropdown(f);
|
|
||||||
}
|
|
||||||
//rootFilters.forEach(f => await createDropdown(f));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createDropdown(config) {
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.id = `container_${config.id}`;
|
|
||||||
|
|
||||||
const label = document.createElement("label");
|
|
||||||
label.textContent = config.label + ": ";
|
|
||||||
label.style.fontWeight = "bold";
|
|
||||||
|
|
||||||
const select = document.createElement("select");
|
|
||||||
select.id = config.id;
|
|
||||||
select.innerHTML = `<option value="">Select...</option>`;
|
|
||||||
|
|
||||||
// Marked callback as async
|
|
||||||
select.addEventListener("change", async (e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
currentSelections[config.id] = val;
|
|
||||||
|
|
||||||
// 1. Clear dependent filters and data
|
|
||||||
clearDependents(config.id);
|
|
||||||
|
|
||||||
// 2. Load options for children (Fixed with for...of)
|
|
||||||
const children = manifest.filters.filter(f => f.depends_on === config.id);
|
|
||||||
for (const child of children) {
|
|
||||||
const options = await fetchOptions(child, val);
|
|
||||||
populateDropdown(child.id, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update data grid
|
|
||||||
updateGrid();
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(label);
|
|
||||||
container.appendChild(select);
|
|
||||||
document.getElementById("filter-area").appendChild(container);
|
|
||||||
// Special handling for parent items:
|
|
||||||
if (config.depends_on === null) {
|
|
||||||
var options = await fetchOptions(config);
|
|
||||||
populateDropdown(select.id, options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchOptions(config, value) {
|
|
||||||
var params
|
|
||||||
if (config.filter_column == undefined) {
|
|
||||||
console.log(config + "Undefined filter col");
|
|
||||||
params = new URLSearchParams({
|
|
||||||
table: config.table,
|
|
||||||
pluck_column: config.pluck_column,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
params = new URLSearchParams({
|
|
||||||
table: config.table,
|
|
||||||
pluck_column: config.pluck_column,
|
|
||||||
filter_column: config.filter_column,
|
|
||||||
value: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const resp = await fetch(`/dashboard/options?${params.toString()}`);
|
|
||||||
return await resp.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateDropdown(id, options) {
|
|
||||||
const select = document.getElementById(id) || createMissingDropdown(id);
|
|
||||||
select.innerHTML = `<option value="">Select...</option>`;
|
|
||||||
options.forEach(opt => {
|
|
||||||
select.innerHTML += `<option value="${opt}">${opt}</option>`;
|
|
||||||
});
|
|
||||||
// If this is the last dropdown, it should be visible now
|
|
||||||
const container = document.getElementById(`container_${id}`);
|
|
||||||
if(container) container.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMissingDropdown(id) {
|
|
||||||
const config = manifest.filters.find(f => f.id === id);
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.id = `container_${id}`;
|
|
||||||
container.style.display = "none"; // Hidden until parent is selected
|
|
||||||
|
|
||||||
const label = document.createElement("label");
|
|
||||||
label.textContent = config.label + ": ";
|
|
||||||
|
|
||||||
const select = document.createElement("select");
|
|
||||||
select.id = id;
|
|
||||||
|
|
||||||
// Marked callback as async
|
|
||||||
select.addEventListener("change", async (e) => {
|
|
||||||
currentSelections[id] = e.target.value;
|
|
||||||
clearDependents(id);
|
|
||||||
const children = manifest.filters.filter(f => f.depends_on === id);
|
|
||||||
|
|
||||||
// Fixed with for...of loop
|
|
||||||
for (const child of children) {
|
|
||||||
const opts = await fetchOptions(child, e.target.value);
|
|
||||||
populateDropdown(child.id, opts);
|
|
||||||
}
|
|
||||||
updateGrid();
|
|
||||||
});
|
|
||||||
|
|
||||||
container.appendChild(label);
|
|
||||||
container.appendChild(select);
|
|
||||||
document.getElementById("filter-area").appendChild(container);
|
|
||||||
return select;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDependents(id) {
|
|
||||||
// Recursively find all filters that depend on this ID and reset them
|
|
||||||
manifest.filters.forEach(f => {
|
|
||||||
if (f.depends_on === id) {
|
|
||||||
const select = document.getElementById(f.id);
|
|
||||||
if (select) select.innerHTML = `<option value="">Select...</option>`;
|
|
||||||
const container = document.getElementById(`container_${f.id}`);
|
|
||||||
if (container) container.style.display = "none";
|
|
||||||
delete currentSelections[f.id];
|
|
||||||
clearDependents(f.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateGrid() {
|
|
||||||
const params = new URLSearchParams(currentSelections);
|
|
||||||
const resp = await fetch(`/dashboard/data?${params.toString()}`);
|
|
||||||
const data = await resp.json();
|
|
||||||
renderLayout(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLayout(data) {
|
|
||||||
const area = document.getElementById("results-area");
|
|
||||||
area.innerHTML = "";
|
|
||||||
|
|
||||||
// Group layout by grid name (defaults to 'main')
|
|
||||||
const grids = {};
|
|
||||||
manifest.layout.forEach(item => {
|
|
||||||
const gridId = item.grid || 'main';
|
|
||||||
if (!grids[gridId]) grids[gridId] = [];
|
|
||||||
grids[gridId].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(grids).forEach(gridId => {
|
|
||||||
const table = document.createElement("table");
|
|
||||||
table.className = "dyn-grid";
|
|
||||||
|
|
||||||
const layoutItems = grids[gridId];
|
|
||||||
const maxRow = Math.max(...layoutItems.map(i => i.row));
|
|
||||||
|
|
||||||
for (let r = 0; r <= maxRow; r++) {
|
|
||||||
const tr = document.createElement("tr");
|
|
||||||
const rowItems = layoutItems.filter(i => i.row === r);
|
|
||||||
|
|
||||||
rowItems.forEach(item => {
|
|
||||||
const td = document.createElement("td");
|
|
||||||
td.className = item.class;
|
|
||||||
td.colSpan = item.colspan || 1;
|
|
||||||
td.rowSpan = item.rowspan || 1;
|
|
||||||
|
|
||||||
if (item.text) {
|
|
||||||
td.textContent = item.text;
|
|
||||||
} else if (item.value) {
|
|
||||||
// Resolve nested JSON path (e.g., "main_info.joint")
|
|
||||||
const parts = item.value.split('.');
|
|
||||||
const val = parts.reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : '', data);
|
|
||||||
td.textContent = val;
|
|
||||||
}
|
|
||||||
tr.appendChild(td);
|
|
||||||
});
|
|
||||||
table.appendChild(tr);
|
|
||||||
}
|
|
||||||
area.appendChild(table);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<h1>Excel Replacement Dashboard</h1>
|
||||||
|
|
||||||
|
<!-- Dropdown Filters Form -->
|
||||||
|
<%= form_with url: dashboard_path, method: :get, local: true do |f| %>
|
||||||
|
<div>
|
||||||
|
<%= f.label :category, "Select Category:" %>
|
||||||
|
<%= f.select :category, options_for_select(@available_categories, params[:category]), include_blank: "All Categories" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= f.label :region, "Select Region:" %>
|
||||||
|
<%= f.select :region, options_for_select(@available_regions, params[:region]), include_blank: "All Regions" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= f.submit "Filter Data" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Displaying the Data in Specific Locations / Table -->
|
||||||
|
<h2>Dashboard Results</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Column 1</th>
|
||||||
|
<th>Column 2</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @records.each do |record| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= record.category_column %></td>
|
||||||
|
<td><%= record.region_column %></td>
|
||||||
|
<td><%= record.metric_value_column %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -23,5 +23,8 @@ module Site
|
|||||||
#
|
#
|
||||||
# config.time_zone = "Central Time (US & Canada)"
|
# config.time_zone = "Central Time (US & Canada)"
|
||||||
# config.eager_load_paths << Rails.root.join("extras")
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
|
|
||||||
|
# Disable activerecord performing migrations.
|
||||||
|
config.active_record.migration_error = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# Filter Hierarchy: Defines the dropdowns and their dependencies
|
|
||||||
filters:
|
|
||||||
- id: "joint"
|
|
||||||
label: "Select Joint"
|
|
||||||
table: "Joint"
|
|
||||||
pluck_column: "Joint"
|
|
||||||
depends_on: null # Root level
|
|
||||||
- id: "joint_info"
|
|
||||||
label: "Select kinematics"
|
|
||||||
table: "JointInfo" # A separate table in SQLite
|
|
||||||
pluck_column: "Osteokinematics"
|
|
||||||
depends_on: "joint" # Only appears after 'joint' is selected
|
|
||||||
filter_column: "Joint" # Filter SubJoint where joint_id == selected joint value
|
|
||||||
|
|
||||||
# Data Mapping: Defines which tables to hit for the final grid
|
|
||||||
data_sources:
|
|
||||||
- table: "Joint"
|
|
||||||
key: "main_info" # Nested JSON key
|
|
||||||
filter_column: "joint"
|
|
||||||
- table: "JointInfo" # MUST MATCH DATABASE TABLE
|
|
||||||
key: "joint_details"
|
|
||||||
filter_column: "Osteokinematics"
|
|
||||||
|
|
||||||
# Layout: Defines the visual grid
|
|
||||||
# colors: dark_blue, light_blue, pale_yellow, pale_green, pale_orange
|
|
||||||
layout:
|
|
||||||
- { row: 0, col: 0, colspan: 3, rowspan: 1, text: "Joint Information", class: "bg-dark-blue" }
|
|
||||||
- { row: 1, col: 0, colspan: 2, rowspan: 1, text: "Anatomical Name", class: "bg-light-blue" }
|
|
||||||
- { row: 2, col: 0, colspan: 2, rowspan: 1, value: "main_info.Joint", class: "bg-pale-yellow" }
|
|
||||||
- { row: 1, col: 2, colspan: 1, rowspan: 1, text: "Common Name", class: "bg-light-blue" }
|
|
||||||
- { row: 2, col: 2, colspan: 1, rowspan: 1, value: "main_info.Common Name", class: "bg-pale-yellow" }
|
|
||||||
- { row: 3, col: 0, colspan: 1, rowspan: 1, text: "Diarthrodial Joint Type", class: "bg-light-blue" }
|
|
||||||
- { row: 4, col: 0, colspan: 1, rowspan: 1, value: "main_info.Diarthrodial joint", class: "bg-pale-yellow" }
|
|
||||||
- { row: 3, col: 1, colspan: 1, rowspan: 1, text: "Alternate Name", class: "bg-light-blue" }
|
|
||||||
- { row: 4, col: 1, colspan: 1, rowspan: 1, value: "main_info.Alternate Name", class: "bg-pale-yellow" }
|
|
||||||
- { row: 3, col: 2, colspan: 1, rowspan: 1, text: "Planes of Freedom", class: "bg-light-blue" }
|
|
||||||
- { row: 4, col: 2, colspan: 1, rowspan: 1, value: "main_info.# of Plains", class: "bg-pale-yellow" }
|
|
||||||
- { row: 5, col: 0, colspan: 1, rowspan: 1, text: "Arthrokinematic Rule", class: "bg-light-blue"}
|
|
||||||
- { row: 6, col: 0, colspan: 1, rowspan: 1, value: "joint_details.Arthrokinematic Rule", class: "bg-pale-yellow"}
|
|
||||||
#- { row: 5, col: 0, colspan: 4, rowspan: 1, value: "main_info.notes", class: "bg-pale-green" }
|
|
||||||
|
|
||||||
# Ligaments Table (separate grid)
|
|
||||||
#- { grid: "ligaments", row: 0, col: 0, colspan: 1, rowspan: 1, text: "Ligaments", class: "bg-dark-blue" }
|
|
||||||
#- { grid: "ligaments", row: 0, col: 1, colspan: 1, rowspan: 1, value: ".list", class: "bg-pale-orange" }
|
|
||||||
|
|
||||||
|
|
||||||
# http://localhost:3000/dashboard/options?table=JointInfo&pluck_column=Osteokinematics&filter_col=undefined&value=Glenohumeral
|
|
||||||
# http://localhost:3000/dashboard/options?table=JointInfo&pluck_column=Osteokinematics&filter_col=Common%20Joint%20Name&value=Shoulder
|
|
||||||
+4
-3
@@ -11,10 +11,11 @@ default: &default
|
|||||||
|
|
||||||
development:
|
development:
|
||||||
adapter: sqlite3
|
adapter: sqlite3
|
||||||
max_connections: 5
|
#database: "file:db/anat_proj.db?mode=ro"
|
||||||
timeout: 5000
|
database: "db/anat_proj.db"
|
||||||
database: storage/anat_proj.db
|
|
||||||
#readonly: true
|
#readonly: true
|
||||||
|
pool: 5
|
||||||
|
timeout: 5000
|
||||||
|
|
||||||
# Warning: The database defined as "test" will be erased and
|
# Warning: The database defined as "test" will be erased and
|
||||||
# re-generated from your development database when you run "rake".
|
# re-generated from your development database when you run "rake".
|
||||||
|
|||||||
@@ -75,4 +75,8 @@ Rails.application.configure do
|
|||||||
|
|
||||||
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
|
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
|
||||||
# config.generators.apply_rubocop_autocorrect_after_generate!
|
# config.generators.apply_rubocop_autocorrect_after_generate!
|
||||||
|
|
||||||
|
config.active_record.migration_error = false
|
||||||
|
config.log_level = :debug
|
||||||
|
#config.active_record,verbose_query_logs = true
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-4
@@ -11,8 +11,5 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
# Defines the root path route ("/")
|
# Defines the root path route ("/")
|
||||||
# root "posts#index"
|
# root "posts#index"
|
||||||
root "dashboard#index"
|
root "joints#index"
|
||||||
get "dashboard", to: "dashboard#index"
|
|
||||||
get "dashboard/options", to: "dashboard#options"
|
|
||||||
get "dashboard/data", to: "dashboard#data"
|
|
||||||
end
|
end
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class DashboardControllerTest < ActionDispatch::IntegrationTest
|
|
||||||
test "options returns JSON" do
|
|
||||||
get "/dashboard/options",
|
|
||||||
params: {
|
|
||||||
table: 'Joint',
|
|
||||||
pluck_column: 'Joint'
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_response :success
|
|
||||||
assert_equal "application/json", response.media_type
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Vendored
-11
@@ -1,11 +0,0 @@
|
|||||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
|
||||||
|
|
||||||
# This model initially had no columns defined. If you add columns to the
|
|
||||||
# model remove the "{}" from the fixture names and add the columns immediately
|
|
||||||
# below each fixture, per the syntax in the comments below
|
|
||||||
#
|
|
||||||
one: {}
|
|
||||||
# column: value
|
|
||||||
#
|
|
||||||
two: {}
|
|
||||||
# column: value
|
|
||||||
Vendored
-11
@@ -1,11 +0,0 @@
|
|||||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
|
||||||
|
|
||||||
# This model initially had no columns defined. If you add columns to the
|
|
||||||
# model remove the "{}" from the fixture names and add the columns immediately
|
|
||||||
# below each fixture, per the syntax in the comments below
|
|
||||||
#
|
|
||||||
one: {}
|
|
||||||
# column: value
|
|
||||||
#
|
|
||||||
two: {}
|
|
||||||
# column: value
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class JointTest < ActiveSupport::TestCase
|
|
||||||
# test "the truth" do
|
|
||||||
# assert true
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class JointsInfoTest < ActiveSupport::TestCase
|
class JointsDashboardTest < ActiveSupport::TestCase
|
||||||
# test "the truth" do
|
# test "the truth" do
|
||||||
# assert true
|
# assert true
|
||||||
# end
|
# end
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
require "test_helper"
|
|
||||||
|
|
||||||
class SpecialTestTest < ActiveSupport::TestCase
|
|
||||||
# test "the truth" do
|
|
||||||
# assert true
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user