Merge pull request 'Add fuzzy search using WFA' (#2) from wfa-fuzzy-search into main
Reviewed-on: #2
This commit is contained in:
commit
bbfc77dc04
@ -52,4 +52,4 @@ input[type="radio"] {
|
|||||||
|
|
||||||
div[draggable="true"] {
|
div[draggable="true"] {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
@ -111,4 +111,16 @@ hr, * {
|
|||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
min-height: 1em;
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w3-select, select {
|
||||||
|
padding: 8px;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E:root%7Bcolor:%23fff%7D@media (prefers-color-scheme:light)%7B:root%7Bcolor:%23000%7D%7D%3C/style%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M12.707 14.707a1 1 0 01-1.414 0l-5-5a1 1 0 011.414-1.414L12 12.586l4.293-4.293a1 1 0 111.414 1.414l-5 5z' fill='currentColor'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 8px top 50%;
|
||||||
|
background-size: 1em auto;
|
||||||
}
|
}
|
@ -12,6 +12,7 @@
|
|||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<script src="scripts/index.js" type="module"></script>
|
<script src="scripts/index.js" type="module"></script>
|
||||||
<script src="scripts/instance.js" type="module"></script>
|
<script src="scripts/instance.js" type="module"></script>
|
||||||
|
<script src="modules/wfa.js" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
#instance-container > div {
|
#instance-container > div {
|
||||||
border-bottom: 1px solid white;
|
border-bottom: 1px solid white;
|
||||||
|
328
modules/wfa.js
Normal file
328
modules/wfa.js
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
class WavefrontComponent {
|
||||||
|
constructor () {
|
||||||
|
this.lo = [0]; // lo for each wavefront
|
||||||
|
this.hi = [0]; // hi for each wavefront
|
||||||
|
this.W = []; // wavefront diag distance for each wavefront
|
||||||
|
this.A = []; // compact CIGAR for backtrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// get value for wavefront=score, diag=k
|
||||||
|
get_val (score, k) {
|
||||||
|
if (this.W[score] !== undefined && this.W[score][k] !== undefined) {
|
||||||
|
return this.W[score][k];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set value for wavefront=score, diag=k
|
||||||
|
set_val (score, k, val) {
|
||||||
|
if (this.W[score]) {
|
||||||
|
this.W[score][k] = val;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.W[score] = [];
|
||||||
|
this.W[score][k] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get alignment traceback
|
||||||
|
get_traceback (score, k) {
|
||||||
|
if (this.A[score] !== undefined && this.A[score][k] !== undefined) {
|
||||||
|
return this.A[score][k];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set alignment traceback
|
||||||
|
set_traceback (score, k, traceback) {
|
||||||
|
if (this.A[score]) {
|
||||||
|
this.A[score][k] = traceback;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.A[score] = [];
|
||||||
|
this.A[score][k] = traceback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get hi for wavefront=score
|
||||||
|
get_hi (score) {
|
||||||
|
const hi = this.hi[score];
|
||||||
|
return isNaN(hi) ? 0 : hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set hi for wavefront=score
|
||||||
|
set_hi (score, hi) {
|
||||||
|
this.hi[score] = hi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get lo for wavefront=score
|
||||||
|
get_lo (score) {
|
||||||
|
const lo = this.lo[score];
|
||||||
|
return isNaN(lo) ? 0 : lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set lo for wavefront=score
|
||||||
|
set_lo (score, lo) {
|
||||||
|
this.lo[score] = lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// string representation of all wavefronts
|
||||||
|
toString () {
|
||||||
|
const traceback_str = ["OI", "EI", "OD", "ED", "SB", "IN", "DL", "EN"];
|
||||||
|
let s = "<";
|
||||||
|
let min_lo = Infinity;
|
||||||
|
let max_hi = -Infinity;
|
||||||
|
// get the min lo and max hi values across all wavefronts
|
||||||
|
for (let i = 0; i < this.W.length; i++) {
|
||||||
|
const lo = this.lo[i];
|
||||||
|
const hi = this.hi[i];
|
||||||
|
if (lo < min_lo) {
|
||||||
|
min_lo = lo;
|
||||||
|
}
|
||||||
|
if (hi > max_hi) {
|
||||||
|
max_hi = hi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// print out two headers, one for wavefront and one for traceback
|
||||||
|
for (let k = min_lo; k <= max_hi; k++) {
|
||||||
|
s += FormatNumberLength(k, 2);
|
||||||
|
if (k < max_hi) {
|
||||||
|
s += "|";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s += ">\t<";
|
||||||
|
for (let k = min_lo; k <= max_hi; k++) {
|
||||||
|
s += FormatNumberLength(k, 2);
|
||||||
|
if (k < max_hi) {
|
||||||
|
s += "|";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s += ">\n";
|
||||||
|
// for each wavefront
|
||||||
|
for (let i = 0; i < this.W.length; i++) {
|
||||||
|
s += "[";
|
||||||
|
const lo = this.lo[i];
|
||||||
|
const hi = this.hi[i];
|
||||||
|
// print out the wavefront matrix
|
||||||
|
for (let k = min_lo; k <= max_hi; k++) {
|
||||||
|
if (this.W[i] !== undefined && this.W[i][k] !== undefined && !isNaN(this.W[i][k])) {
|
||||||
|
s += FormatNumberLength(this.W[i][k], 2);
|
||||||
|
}
|
||||||
|
else if (k < lo || k > hi) {
|
||||||
|
s += "--";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s += " ";
|
||||||
|
}
|
||||||
|
if (k < max_hi) {
|
||||||
|
s += "|";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s += "]\t[";
|
||||||
|
// print out the traceback matrix
|
||||||
|
for (let k = min_lo; k <= max_hi; k++) {
|
||||||
|
if (this.A[i] !== undefined && this.A[i][k] !== undefined) {
|
||||||
|
s += traceback_str[this.A[i][k].toString()];
|
||||||
|
}
|
||||||
|
else if (k < lo || k > hi) {
|
||||||
|
s += "--";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
s += " ";
|
||||||
|
}
|
||||||
|
if (k < max_hi) {
|
||||||
|
s += "|";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s += "]\n";
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const traceback = {
|
||||||
|
OpenIns: 0,
|
||||||
|
ExtdIns: 1,
|
||||||
|
OpenDel: 2,
|
||||||
|
ExtdDel: 3,
|
||||||
|
Sub: 4,
|
||||||
|
Ins: 5,
|
||||||
|
Del: 6,
|
||||||
|
End: 7
|
||||||
|
};
|
||||||
|
|
||||||
|
function FormatNumberLength (num, length) {
|
||||||
|
let r = "" + num;
|
||||||
|
while (r.length < length) {
|
||||||
|
r = " " + r;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function min (args) {
|
||||||
|
args.forEach((el, idx, arr) => {
|
||||||
|
arr[idx] = isNaN(el) ? Infinity : el;
|
||||||
|
});
|
||||||
|
const min = Math.min.apply(Math, args);
|
||||||
|
return min === Infinity ? NaN : min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function max (args) {
|
||||||
|
args.forEach((el, idx, arr) => {
|
||||||
|
arr[idx] = isNaN(el) ? -Infinity : el;
|
||||||
|
});
|
||||||
|
const max = Math.max.apply(Math, args);
|
||||||
|
return max === -Infinity ? NaN : max;
|
||||||
|
}
|
||||||
|
|
||||||
|
function argmax (args) {
|
||||||
|
const val = max(args);
|
||||||
|
return args.indexOf(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function wf_align (s1, s2, penalties) {
|
||||||
|
const n = s1.length;
|
||||||
|
const m = s2.length;
|
||||||
|
const A_k = m - n;
|
||||||
|
const A_offset = m;
|
||||||
|
let score = 0;
|
||||||
|
const M = new WavefrontComponent();
|
||||||
|
M.set_val(0, 0, 0);
|
||||||
|
M.set_hi(0, 0);
|
||||||
|
M.set_lo(0, 0);
|
||||||
|
M.set_traceback(0, 0, traceback.End);
|
||||||
|
const I = new WavefrontComponent();
|
||||||
|
const D = new WavefrontComponent();
|
||||||
|
while (true) {
|
||||||
|
wf_extend(M, s1, n, s2, m, score);
|
||||||
|
if (M.get_val(score, A_k) >= A_offset) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
score++;
|
||||||
|
wf_next(M, I, D, score, penalties);
|
||||||
|
}
|
||||||
|
return wf_backtrace(M, I, D, score, penalties, A_k, A_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wf_extend (M, s1, n, s2, m, score) {
|
||||||
|
const lo = M.get_lo(score);
|
||||||
|
const hi = M.get_hi(score);
|
||||||
|
for (let k = lo; k <= hi; k++) {
|
||||||
|
let v = M.get_val(score, k) - k;
|
||||||
|
let h = M.get_val(score, k);
|
||||||
|
if (isNaN(v) || isNaN(h)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (s1[v] === s2[h]) {
|
||||||
|
M.set_val(score, k, M.get_val(score, k) + 1);
|
||||||
|
v++;
|
||||||
|
h++;
|
||||||
|
if (v > n || h > m) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wf_next (M, I, D, score, penalties) {
|
||||||
|
const x = penalties.x;
|
||||||
|
const o = penalties.o;
|
||||||
|
const e = penalties.e;
|
||||||
|
const lo = min([M.get_lo(score - x), M.get_lo(score - o - e), I.get_lo(score - e), D.get_lo(score - e)]) - 1;
|
||||||
|
const hi = max([M.get_hi(score - x), M.get_hi(score - o - e), I.get_hi(score - e), D.get_hi(score - e)]) + 1;
|
||||||
|
M.set_hi(score, hi);
|
||||||
|
I.set_hi(score, hi);
|
||||||
|
D.set_hi(score, hi);
|
||||||
|
M.set_lo(score, lo);
|
||||||
|
I.set_lo(score, lo);
|
||||||
|
D.set_lo(score, lo);
|
||||||
|
for (let k = lo; k <= hi; k++) {
|
||||||
|
I.set_val(score, k, max([
|
||||||
|
M.get_val(score - o - e, k - 1),
|
||||||
|
I.get_val(score - e, k - 1)
|
||||||
|
]) + 1);
|
||||||
|
I.set_traceback(score, k, [traceback.OpenIns, traceback.ExtdIns][argmax([
|
||||||
|
M.get_val(score - o - e, k - 1),
|
||||||
|
I.get_val(score - e, k - 1)
|
||||||
|
])]);
|
||||||
|
D.set_val(score, k, max([
|
||||||
|
M.get_val(score - o - e, k + 1),
|
||||||
|
D.get_val(score - e, k + 1)
|
||||||
|
]));
|
||||||
|
D.set_traceback(score, k, [traceback.OpenDel, traceback.ExtdDel][argmax([
|
||||||
|
M.get_val(score - o - e, k + 1),
|
||||||
|
D.get_val(score - e, k + 1)
|
||||||
|
])]);
|
||||||
|
M.set_val(score, k, max([
|
||||||
|
M.get_val(score - x, k) + 1,
|
||||||
|
I.get_val(score, k),
|
||||||
|
D.get_val(score, k)
|
||||||
|
]));
|
||||||
|
M.set_traceback(score, k, [traceback.Sub, traceback.Ins, traceback.Del][argmax([
|
||||||
|
M.get_val(score - x, k) + 1,
|
||||||
|
I.get_val(score, k),
|
||||||
|
D.get_val(score, k)
|
||||||
|
])]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wf_backtrace (M, I, D, score, penalties, A_k) {
|
||||||
|
const traceback_CIGAR = ["I", "I", "D", "D", "X", "", "", ""];
|
||||||
|
const x = penalties.x;
|
||||||
|
const o = penalties.o;
|
||||||
|
const e = penalties.e;
|
||||||
|
let CIGAR_rev = ""; // reversed CIGAR
|
||||||
|
let tb_s = score; // traceback score
|
||||||
|
let tb_k = A_k; // traceback diag k
|
||||||
|
let current_traceback = M.get_traceback(tb_s, tb_k);
|
||||||
|
let done = false;
|
||||||
|
while (!done) {
|
||||||
|
CIGAR_rev += traceback_CIGAR[current_traceback];
|
||||||
|
switch (current_traceback) {
|
||||||
|
case traceback.OpenIns:
|
||||||
|
tb_s = tb_s - o - e;
|
||||||
|
tb_k = tb_k - 1;
|
||||||
|
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.ExtdIns:
|
||||||
|
tb_s = tb_s - e;
|
||||||
|
tb_k = tb_k - 1;
|
||||||
|
current_traceback = I.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.OpenDel:
|
||||||
|
tb_s = tb_s - o - e;
|
||||||
|
tb_k = tb_k + 1;
|
||||||
|
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.ExtdDel:
|
||||||
|
tb_s = tb_s - e;
|
||||||
|
tb_k = tb_k + 1;
|
||||||
|
current_traceback = D.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.Sub:
|
||||||
|
tb_s = tb_s - x;
|
||||||
|
// tb_k = tb_k;
|
||||||
|
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.Ins:
|
||||||
|
// tb_s = tb_s;
|
||||||
|
// tb_k = tb_k;
|
||||||
|
current_traceback = I.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.Del:
|
||||||
|
// tb_s = tb_s;
|
||||||
|
// tb_k = tb_k;
|
||||||
|
current_traceback = D.get_traceback(tb_s, tb_k);
|
||||||
|
break;
|
||||||
|
case traceback.End:
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const CIGAR = Array.from(CIGAR_rev).reverse().join("");
|
||||||
|
return { CIGAR, score };
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
"description": "Front-end for ProxmoxAAS",
|
"description": "Front-end for ProxmoxAAS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix ."
|
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.43.0",
|
"eslint": "^8.43.0",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js";
|
import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js";
|
||||||
import { alert, dialog } from "./dialog.js";
|
import { alert, dialog } from "./dialog.js";
|
||||||
import { setupClientSync } from "./clientsync.js";
|
import { setupClientSync } from "./clientsync.js";
|
||||||
|
import wf_align from "../modules/wfa.js";
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", init);
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
@ -38,6 +39,11 @@ async function getInstances () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function populateInstances () {
|
async function populateInstances () {
|
||||||
|
let searchCriteria = localStorage.getItem("search-criteria");
|
||||||
|
if (!searchCriteria) {
|
||||||
|
searchCriteria = "fuzzy";
|
||||||
|
localStorage.setItem("search-criteria", "fuzzy");
|
||||||
|
}
|
||||||
const searchQuery = document.querySelector("#search").value || null;
|
const searchQuery = document.querySelector("#search").value || null;
|
||||||
let criteria;
|
let criteria;
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
@ -45,7 +51,7 @@ async function populateInstances () {
|
|||||||
return (a.vmid > b.vmid) ? 1 : -1;
|
return (a.vmid > b.vmid) ? 1 : -1;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else {
|
else if (searchCriteria === "exact") {
|
||||||
criteria = (a, b) => {
|
criteria = (a, b) => {
|
||||||
const aInc = a.name.toLowerCase().includes(searchQuery.toLowerCase());
|
const aInc = a.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const bInc = b.name.toLowerCase().includes(searchQuery.toLowerCase());
|
const bInc = b.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
@ -63,6 +69,27 @@ async function populateInstances () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if (searchCriteria === "fuzzy") {
|
||||||
|
const penalties = {
|
||||||
|
m: 0,
|
||||||
|
x: 1,
|
||||||
|
o: 1,
|
||||||
|
e: 1
|
||||||
|
};
|
||||||
|
criteria = (a, b) => {
|
||||||
|
// lower is better
|
||||||
|
const aAlign = wf_align(a.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
|
||||||
|
const aScore = aAlign.score / a.name.length;
|
||||||
|
const bAlign = wf_align(b.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
|
||||||
|
const bScore = bAlign.score / b.name.length;
|
||||||
|
if (aScore === bScore) {
|
||||||
|
return a.vmid > b.vmid ? 1 : -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return aScore - bScore;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
instances.sort(criteria);
|
instances.sort(criteria);
|
||||||
const instanceContainer = document.querySelector("#instance-container");
|
const instanceContainer = document.querySelector("#instance-container");
|
||||||
instanceContainer.innerHTML = "";
|
instanceContainer.innerHTML = "";
|
||||||
|
@ -12,6 +12,10 @@ function init () {
|
|||||||
if (rate) {
|
if (rate) {
|
||||||
document.querySelector("#sync-rate").value = rate;
|
document.querySelector("#sync-rate").value = rate;
|
||||||
}
|
}
|
||||||
|
const search = localStorage.getItem("search-criteria");
|
||||||
|
if (search) {
|
||||||
|
document.querySelector(`#search-${search}`).checked = true;
|
||||||
|
}
|
||||||
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
|
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,5 +24,6 @@ function handleSaveSettings (event) {
|
|||||||
const form = new FormData(document.querySelector("#settings"));
|
const form = new FormData(document.querySelector("#settings"));
|
||||||
localStorage.setItem("sync-scheme", form.get("sync-scheme"));
|
localStorage.setItem("sync-scheme", form.get("sync-scheme"));
|
||||||
localStorage.setItem("sync-rate", form.get("sync-rate"));
|
localStorage.setItem("sync-rate", form.get("sync-rate"));
|
||||||
|
localStorage.setItem("search-criteria", form.get("search-criteria"));
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
@ -51,10 +51,10 @@
|
|||||||
<h3>Synchronization Settings</h3>
|
<h3>Synchronization Settings</h3>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>App Sync Method</legend>
|
<legend>App Sync Method</legend>
|
||||||
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required><span>Always Sync</span></label>
|
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label>
|
||||||
<p>App will always periodically synchronize with Proxmox. High resource usage.</p>
|
<p>App will always periodically synchronize with Proxmox. High resource usage.</p>
|
||||||
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
|
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
|
||||||
<p>App will periodically synchronize only if there have been change. Medium resource usage.</p>
|
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
|
||||||
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
||||||
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -65,6 +65,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w3-card w3-padding">
|
||||||
|
<h3>Search Settings</h3>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Instance Search Criteria</legend>
|
||||||
|
<label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label>
|
||||||
|
<p>Sorts by exact query match in instance name.</p>
|
||||||
|
<label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label>
|
||||||
|
<p>Sorts by best matching to worst matching.</p>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
<div class="w3-container w3-center" id="form-actions">
|
<div class="w3-container w3-center" id="form-actions">
|
||||||
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
|
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user