Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f62ce57279 | |||
| f497329169 | |||
| f55348f0b9 | |||
| 7ebf6dbf41 | |||
| 0b4532282d | |||
| d6c8fdb7ae | |||
| 03bb708e1f | |||
| 0c392e5c71 | |||
| 0fbd307749 | |||
| 76977b9e90 | |||
| 01ada2bf65 |
@@ -0,0 +1,67 @@
|
|||||||
|
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 ]
|
branches: [ main, second_try ]
|
||||||
|
|
||||||
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: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
run: apt-get update && 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: sudo apt-get update && sudo apt-get install --no-install-recommends -y libvips
|
run: apt-get update && apt-get install --no-install-recommends -y libvips
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|||||||
@@ -1,31 +1,78 @@
|
|||||||
|
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
|
class DashboardController < ApplicationController
|
||||||
|
before_action :load_manifest
|
||||||
|
|
||||||
def index
|
def index
|
||||||
joint_column = 'joint'
|
# Renders the main page
|
||||||
# Grab distinct values for dropdown filters
|
|
||||||
@available_joints = Joint.distinct.pluck(joint_column)
|
|
||||||
|
|
||||||
# Fetch initial records
|
|
||||||
@records = filter_records
|
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html # Renders app/views/dashboards/index.html.erb
|
format.html
|
||||||
format.json { render json: @records } # Returns filtered data as JSON for JS
|
format.json { render json: @manifest }
|
||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def filter_records
|
def load_manifest
|
||||||
records = Joint.all
|
@manifest = YAML.load_file(Rails.root.join('config', 'dashboard_manifest.yml'))
|
||||||
records = records.where(joint: params[:joint]) if params[:joint].present?
|
|
||||||
records
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# old broken: def index
|
|
||||||
# old broken: @joint = Joint.distinct.pluck(:Joint)
|
|
||||||
# old broken:
|
|
||||||
# old broken: @records = Joint.all
|
|
||||||
# old broken:
|
|
||||||
# old broken: @records = @records.where(Joint: params[joint]) if params[:joint].present?
|
|
||||||
# old broken: end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class JointInfo < ApplicationRecord
|
||||||
|
def readonly?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class JointsInfo < ApplicationRecord
|
|
||||||
def readonly?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,88 +1,219 @@
|
|||||||
<h1>Excel Replacement Dashboard</h1>
|
<h1 style="font-family: Arial, sans-serif;">Dynamic Dashboard</h1>
|
||||||
|
|
||||||
<!-- Dropdown Filters (No submit button needed, JS handles it) -->
|
<div id="filter-area" style="margin-bottom: 20px; display: flex; gap: 15px; font-family: Arial, sans-serif;">
|
||||||
<%= form_with url: dashboard_path, method: :get, id: "filter-form" do |f| %>
|
<!-- Dropdowns injected here by JS -->
|
||||||
<div>
|
|
||||||
<%= f.label :joint, "Select Joint:" %>
|
|
||||||
<%= f.select :joint, options_for_select(@available_joints, params[:joint]), include_blank: "All joints", id: "joint" %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<hr>
|
<div id="results-area" style="display: flex; gap: 20px;">
|
||||||
|
<!-- Grids injected here by JS -->
|
||||||
<h2>Dashboard Results</h2>
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table border="1" id="results-table">
|
|
||||||
<thead>
|
|
||||||
<tr id="table-headers">
|
|
||||||
<!-- Dynamically populated headers -->
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="table-body">
|
|
||||||
<!-- Dynamically populated rows (supports 4 to 20+ values) -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</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>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const jointSelect = document.getElementById("joint");
|
let manifest = {};
|
||||||
const tableHeaders = document.getElementById("table-headers");
|
let currentSelections = {};
|
||||||
const tableBody = document.getElementById("table-body");
|
|
||||||
|
|
||||||
function fetchFilteredData() {
|
async function init() {
|
||||||
const joint = jointSelect.value;
|
const resp = await fetch(
|
||||||
|
'/dashboard',
|
||||||
// Build query parameters
|
{
|
||||||
const params = new URLSearchParams();
|
headers: {
|
||||||
if (joint) params.append("joint", joint);
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
// Fetch data from Rails with JSON format
|
});
|
||||||
fetch(`/dashboard?${params.toString()}`, {
|
manifest = await resp.json();
|
||||||
headers: { "Accept": "application/json" }
|
await renderRootFilters();
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
renderTable(data);
|
|
||||||
})
|
|
||||||
.catch(error => console.error("Error fetching dashboard data:", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(records) {
|
async function renderRootFilters() {
|
||||||
tableHeaders.innerHTML = "";
|
const rootFilters = manifest.filters.filter(f => f.depends_on === null);
|
||||||
tableBody.innerHTML = "";
|
for (const f of rootFilters) {
|
||||||
|
await createDropdown(f);
|
||||||
if (records.length === 0) {
|
}
|
||||||
tableBody.innerHTML = "<tr><td colspan='100'>No records found</td></tr>";
|
//rootFilters.forEach(f => await createDropdown(f));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract column keys dynamically from the first record (handles variable lengths)
|
async function createDropdown(config) {
|
||||||
const columns = Object.keys(records[0]);
|
const container = document.createElement("div");
|
||||||
|
container.id = `container_${config.id}`;
|
||||||
|
|
||||||
// 1. Build dynamic headers
|
const label = document.createElement("label");
|
||||||
columns.forEach(col => {
|
label.textContent = config.label + ": ";
|
||||||
const th = document.createElement("th");
|
label.style.fontWeight = "bold";
|
||||||
th.textContent = col.replace(/_/g, " ").toUpperCase();
|
|
||||||
tableHeaders.appendChild(th);
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Build dynamic rows (whether 4 or 20 attributes long)
|
container.appendChild(label);
|
||||||
records.forEach(record => {
|
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 tr = document.createElement("tr");
|
||||||
columns.forEach(col => {
|
const rowItems = layoutItems.filter(i => i.row === r);
|
||||||
|
|
||||||
|
rowItems.forEach(item => {
|
||||||
const td = document.createElement("td");
|
const td = document.createElement("td");
|
||||||
td.textContent = record[col] !== null ? record[col] : "";
|
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);
|
tr.appendChild(td);
|
||||||
});
|
});
|
||||||
tableBody.appendChild(tr);
|
table.appendChild(tr);
|
||||||
|
}
|
||||||
|
area.appendChild(table);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger fetch when either dropdown changes
|
init();
|
||||||
jointSelect.addEventListener("change", fetchFilteredData);
|
|
||||||
|
|
||||||
// Load initial data on page load
|
|
||||||
fetchFilteredData();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 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
|
||||||
@@ -13,4 +13,6 @@ Rails.application.routes.draw do
|
|||||||
# root "posts#index"
|
# root "posts#index"
|
||||||
root "dashboard#index"
|
root "dashboard#index"
|
||||||
get "dashboard", to: "dashboard#index"
|
get "dashboard", to: "dashboard#index"
|
||||||
|
get "dashboard/options", to: "dashboard#options"
|
||||||
|
get "dashboard/data", to: "dashboard#data"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
require "test_helper"
|
require "test_helper"
|
||||||
|
|
||||||
class DashboardControllerTest < ActionDispatch::IntegrationTest
|
class DashboardControllerTest < ActionDispatch::IntegrationTest
|
||||||
# test "the truth" do
|
test "options returns JSON" do
|
||||||
# assert true
|
get "/dashboard/options",
|
||||||
# end
|
params: {
|
||||||
|
table: 'Joint',
|
||||||
|
pluck_column: 'Joint'
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
assert_equal "application/json", response.media_type
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user