mirror of
https://github.com/hapifhir/hapi-fhir.git
synced 2025-03-09 14:33:32 +00:00
Startup and Testpage Overlay tweaks (#6578)
* Startup and Testpage Overlay tweaks * Add changelog * Account for review comments * Test fix
This commit is contained in:
parent
4feb489735
commit
24abd6bd65
@ -23,6 +23,11 @@ indent_style = tab
|
||||
tab_width = 3
|
||||
indent_size = 3
|
||||
|
||||
[*.js]
|
||||
indent_style = tab
|
||||
tab_width = 3
|
||||
indent_size = 3
|
||||
|
||||
[*.vm]
|
||||
indent_style = tab
|
||||
tab_width = 3
|
||||
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fix
|
||||
issue: 6578
|
||||
title: "The search parameter picker in the Testpage Overlay module has been adjusted to
|
||||
correct several display issues which appeared after the upgrade from Bootstrap 4 to
|
||||
Bootstrap 5."
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
type: fix
|
||||
issue: 6578
|
||||
title: "When starting up the JPA server under heavy load, the validation support cache
|
||||
could perform a large number of identical parallel queries. A synchronization guard
|
||||
has been placed around the cache loader to avoid this."
|
@ -31,6 +31,8 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import ca.uhn.fhir.util.VersionUtil;
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
@ -78,7 +80,7 @@ import static ca.uhn.fhir.util.MessageSupplier.msg;
|
||||
public class GraphQLProviderWithIntrospection extends GraphQLProvider {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProviderWithIntrospection.class);
|
||||
private final GraphQLSchemaGenerator myGenerator;
|
||||
private final Supplier<GraphQLSchemaGenerator> myGenerator;
|
||||
private final ISearchParamRegistry mySearchParamRegistry;
|
||||
private final VersionSpecificWorkerContextWrapper myContext;
|
||||
private final IDaoRegistry myDaoRegistry;
|
||||
@ -99,12 +101,16 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
|
||||
myDaoRegistry = theDaoRegistry;
|
||||
|
||||
myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport);
|
||||
myGenerator = new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion());
|
||||
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.registerTypeAdapter(Collections.emptyList().getClass(), (JsonSerializer<Object>)
|
||||
(src, typeOfSrc, context) -> new JsonArray());
|
||||
myGson = gsonBuilder.create();
|
||||
|
||||
// Lazy-load this because it's expensive, but more importantly because it makes a bunch of
|
||||
// calls for StructureDefinitions and other such things during startup so we want to be sure
|
||||
// that everything else is initialized first
|
||||
myGenerator = Suppliers.memoize(() -> new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -153,37 +159,19 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
|
||||
Collection<String> theResourceTypes,
|
||||
EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) {
|
||||
|
||||
GraphQLSchemaGenerator generator = myGenerator.get();
|
||||
|
||||
final StringBuilder schemaBuilder = new StringBuilder();
|
||||
try (Writer writer = new StringBuilderWriter(schemaBuilder)) {
|
||||
|
||||
// Generate FHIR base types schemas
|
||||
myGenerator.generateTypes(writer, theOperations);
|
||||
generator.generateTypes(writer, theOperations);
|
||||
|
||||
// Fix up a few things that are missing from the generated schema
|
||||
writer.append("\ninterface Element {")
|
||||
.append("\n id: ID")
|
||||
.append("\n}")
|
||||
.append("\n");
|
||||
// writer
|
||||
// .append("\ninterface Quantity {\n")
|
||||
// .append("id: String\n")
|
||||
// .append("extension: [Extension]\n")
|
||||
// .append("value: decimal _value: ElementBase\n")
|
||||
// .append("comparator: code _comparator: ElementBase\n")
|
||||
// .append("unit: String _unit: ElementBase\n")
|
||||
// .append("system: uri _system: ElementBase\n")
|
||||
// .append("code: code _code: ElementBase\n")
|
||||
// .append("\n}")
|
||||
// .append("\n");
|
||||
|
||||
// writer
|
||||
// .append("\ntype Resource {")
|
||||
// .append("\n id: [token]" + "\n}")
|
||||
// .append("\n");
|
||||
// writer
|
||||
// .append("\ninput ResourceInput {")
|
||||
// .append("\n id: [token]" + "\n}")
|
||||
// .append("\n");
|
||||
|
||||
// Generate schemas for the resource types
|
||||
for (String nextResourceType : theResourceTypes) {
|
||||
@ -192,7 +180,7 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
|
||||
.getActiveSearchParams(
|
||||
nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
|
||||
.values());
|
||||
myGenerator.generateResource(writer, sd, parameters, theOperations);
|
||||
generator.generateResource(writer, sd, parameters, theOperations);
|
||||
}
|
||||
|
||||
// Generate queries
|
||||
@ -210,8 +198,8 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
|
||||
.getActiveSearchParams(
|
||||
nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
|
||||
.values());
|
||||
myGenerator.generateListAccessQuery(writer, parameters, nextResourceType);
|
||||
myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType);
|
||||
generator.generateListAccessQuery(writer, parameters, nextResourceType);
|
||||
generator.generateConnectionAccessQuery(writer, parameters, nextResourceType);
|
||||
}
|
||||
}
|
||||
writer.append("\n}");
|
||||
|
@ -285,6 +285,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
|
||||
mySearchProperties.setMaxResultsRequested(theMaxResultsToFetch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDeduplicateInDatabase(boolean theShouldDeduplicateInDB) {
|
||||
mySearchProperties.setDeduplicateInDatabase(theShouldDeduplicateInDB);
|
||||
}
|
||||
|
@ -34,6 +34,8 @@ import org.hl7.fhir.r5.model.Enumerations;
|
||||
import org.hl7.fhir.r5.model.SubscriptionTopic;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -59,6 +61,7 @@ public class SubscriptionTopicLoader extends BaseResourceCacheSynchronizer {
|
||||
}
|
||||
|
||||
@Override
|
||||
@EventListener(classes = ContextRefreshedEvent.class)
|
||||
public void registerListener() {
|
||||
if (!myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4B)) {
|
||||
return;
|
||||
|
@ -19,6 +19,7 @@
|
||||
*/
|
||||
package ca.uhn.fhir.cache;
|
||||
|
||||
import ca.uhn.fhir.IHapiBootOrder;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
|
||||
@ -30,7 +31,6 @@ import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
|
||||
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
@ -40,7 +40,9 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.ContextStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -78,21 +80,34 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi
|
||||
myResourceChangeListenerRegistry = theResourceChangeListenerRegistry;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
/**
|
||||
* This method performs a search in the DB, so use the {@link ContextStartedEvent}
|
||||
* to ensure that it runs after the database initializer
|
||||
*/
|
||||
@EventListener(classes = ContextRefreshedEvent.class)
|
||||
@Order(IHapiBootOrder.AFTER_SUBSCRIPTION_INITIALIZED)
|
||||
public void registerListener() {
|
||||
if (myDaoRegistry.getResourceDaoOrNull(myResourceName) == null) {
|
||||
ourLog.info("No resource DAO found for resource type {}, not registering listener", myResourceName);
|
||||
return;
|
||||
}
|
||||
mySearchParameterMap = getSearchParameterMap();
|
||||
mySystemRequestDetails = SystemRequestDetails.forAllPartitions();
|
||||
|
||||
IResourceChangeListenerCache resourceCache =
|
||||
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
|
||||
myResourceName, mySearchParameterMap, this, REFRESH_INTERVAL);
|
||||
myResourceName, provideSearchParameterMap(), this, REFRESH_INTERVAL);
|
||||
resourceCache.forceRefresh();
|
||||
}
|
||||
|
||||
private SearchParameterMap provideSearchParameterMap() {
|
||||
SearchParameterMap searchParameterMap = mySearchParameterMap;
|
||||
if (searchParameterMap == null) {
|
||||
searchParameterMap = getSearchParameterMap();
|
||||
mySearchParameterMap = searchParameterMap;
|
||||
}
|
||||
return searchParameterMap;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void unregisterListener() {
|
||||
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
|
||||
@ -149,7 +164,7 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi
|
||||
ourLog.debug("Starting sync {}s", myResourceName);
|
||||
|
||||
List<IBaseResource> resourceList = (List<IBaseResource>)
|
||||
getResourceDao().searchForResources(mySearchParameterMap, mySystemRequestDetails);
|
||||
getResourceDao().searchForResources(provideSearchParameterMap(), mySystemRequestDetails);
|
||||
return syncResourcesIntoCache(resourceList);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ca.uhn.fhir.to;
|
||||
|
||||
import ca.uhn.fhir.system.HapiSystemProperties;
|
||||
import ca.uhn.fhir.to.mvc.AnnotationMethodHandlerAdapterConfigurer;
|
||||
import ca.uhn.fhir.to.util.WebUtil;
|
||||
import jakarta.annotation.Nonnull;
|
||||
@ -49,7 +50,7 @@ public class FhirTesterMvcConfig implements WebMvcConfigurer {
|
||||
resolver.setTemplateMode(TemplateMode.HTML);
|
||||
resolver.setCharacterEncoding("UTF-8");
|
||||
|
||||
if (theTesterConfig.getDebugTemplatesMode()) {
|
||||
if (theTesterConfig.getDebugTemplatesMode() || HapiSystemProperties.isUnitTestModeEnabled()) {
|
||||
resolver.setCacheable(false);
|
||||
}
|
||||
|
||||
|
@ -134,69 +134,72 @@
|
||||
<h4>Includes <small>Also include resources which are referenced by the search results</small></h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span th:each="include : ${includes}" class="includeCheckContainer">
|
||||
<span class="includeCheckCheck">
|
||||
<input type="checkbox" th:value="${include}" th:id="'inc_' + ${include}"></input>
|
||||
<div class="col">
|
||||
<span th:each="include : ${includes}" class="includeCheckContainer">
|
||||
<span class="includeCheckCheck">
|
||||
<input type="checkbox" th:value="${include}" th:id="'inc_' + ${include}"></input>
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="${include}"/>
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="${include}"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Sorting -->
|
||||
<br clear="all"/>
|
||||
<div class="row">
|
||||
<h4>Sort Results</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Sort By... -->
|
||||
<div class='col-sm-6'>
|
||||
<label>Sort By</label>
|
||||
<div class="btn-group">
|
||||
<input type="hidden" id="sort_by" />
|
||||
<button type="button" class="btn btn-info" id="search_sort_button">Default</button>
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
<span class="visually-hidden">Default Sort</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="javascript:updateSort('');">Default Sort</a></li>
|
||||
<li class="divider"></li>
|
||||
<li th:each="nextParam : ${sortParams}"><a th:href="'javascript:updateSort(\'' + ${nextParam} + '\');'" th:text="${nextParam}"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='col'>
|
||||
<div class="d-inline-block">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text">Sort By</label>
|
||||
<input type="hidden" id="sort_by" />
|
||||
<label class="input-group-text border border-primary text-primary" id="search_sort_button">Default</label>
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
<span class="visually-hidden">Default Sort</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="javascript:updateSort('');" class="dropdown-item">Default Sort</a></li>
|
||||
<li class="divider"></li>
|
||||
<li th:each="nextParam : ${sortParams}"><a th:href="'javascript:updateSort(\'' + ${nextParam} + '\');'" th:text="${nextParam}" class="dropdown-item"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label>Direction</label>
|
||||
<div class="btn-group">
|
||||
<input type="hidden" id="sort_direction" />
|
||||
<button type="button" class="btn btn-info" id="search_sort_direction_button">Default</button>
|
||||
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
<span class="visually-hidden">Default Sort</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="javascript:updateSortDirection('');">Default</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="javascript:updateSortDirection('asc');">Ascending</a></li>
|
||||
<li><a href="javascript:updateSortDirection('desc');">Descending</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="d-inline-block ms-3">
|
||||
<div class="input-group">
|
||||
<label class="input-group-text">Direction</label>
|
||||
<input type="hidden" id="sort_direction" />
|
||||
<label class="input-group-text border border-primary text-primary" id="search_sort_direction_button">Default</label>
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
<span class="visually-hidden">Default Sort</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li><a href="javascript:updateSortDirection('');" class="dropdown-item">Default</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="javascript:updateSortDirection('asc');" class="dropdown-item">Ascending</a></li>
|
||||
<li><a href="javascript:updateSortDirection('desc');" class="dropdown-item">Descending</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br clear="all"/>
|
||||
|
||||
<!-- Other Options -->
|
||||
<br clear="all"/>
|
||||
<div class="row">
|
||||
<h4>Other Options</h4>
|
||||
<div class="col">
|
||||
<h4>Other Options</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class='col-sm-3'>
|
||||
<div class="form-group">
|
||||
<div class='input-group date'>
|
||||
<div class="input-group-addon">
|
||||
Limit
|
||||
</div>
|
||||
<label class="input-group-text">Limit</label>
|
||||
<input type="text" class="form-control" id="resource-search-limit" placeholder="max # returned"/>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,15 +212,17 @@
|
||||
<h4>Reverse Includes <small>Also include resources which reference to the search results</small></h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="includeCheckCheck">
|
||||
<input type="checkbox" th:value="'*'" th:id="'revinc_STAR'" />
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="'*'"/>
|
||||
<span th:each="include : ${revincludes}" class="includeCheckContainer">
|
||||
<span class="includeCheckContainer">
|
||||
<span class="includeCheckCheck">
|
||||
<input type="checkbox" th:value="${include}" th:id="'revinc_' + ${include}" />
|
||||
<input type="checkbox" th:value="'*'" th:id="'revinc_STAR'" />
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="'*'"/>
|
||||
<span th:each="include : ${revincludes}" class="includeCheckContainer">
|
||||
<span class="includeCheckCheck">
|
||||
<input type="checkbox" th:value="${include}" th:id="'revinc_' + ${include}" />
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="${include}"/>
|
||||
</span>
|
||||
<span class="includeCheckName" th:text="${include}"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,46 +9,46 @@
|
||||
<label class="navBarButtonLabel">Encoding</label>
|
||||
<div class="btn-group" id="encodingBtnGroup" role="group">
|
||||
<input type="radio" class="btn-check" name="encoding" id="encode-default" value="" />
|
||||
<label class="btn btn-info" for="encode-default">(default)</label>
|
||||
<label class="btn btn-outline-info" for="encode-default">(default)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="encoding" id="encode-xml" value="xml" />
|
||||
<label class="btn btn-info" for="encode-xml">XML</label>
|
||||
<label class="btn btn-outline-info" for="encode-xml">XML</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="encoding" id="encode-json" value="json" />
|
||||
<label class="btn btn-info" for="encode-json">JSON</label>
|
||||
<label class="btn btn-outline-info" for="encode-json">JSON</label>
|
||||
</div>
|
||||
|
||||
<!-- Pretty -->
|
||||
<br /> <label class="navBarButtonLabel">Pretty</label>
|
||||
<div role="group" class="btn-group" id="prettyBtnGroup" style="margin-top: 5px;">
|
||||
<input type="radio" class="btn-check" name="pretty" id="pretty-default" value="" />
|
||||
<label class="btn btn-info" for="pretty-default">(default)</label>
|
||||
<label class="btn btn-outline-info" for="pretty-default">(default)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="pretty" id="pretty-true" value="true" />
|
||||
<label class="btn btn-info" for="pretty-true">On</label>
|
||||
<label class="btn btn-outline-info" for="pretty-true">On</label>
|
||||
|
||||
<input
|
||||
type="radio" class="btn-check" name="pretty" id="pretty-false" value="false" />
|
||||
<label class="btn btn-info" for="pretty-false"> Off</label>
|
||||
<label class="btn btn-outline-info" for="pretty-false"> Off</label>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<br /> <label class="navBarButtonLabel">Summary</label>
|
||||
<div role="group" class="btn-group" id="summaryBtnGroup" style="margin-top: 5px;">
|
||||
<input type="radio" class="btn-check" name="_summary" id="summary-default" value="" />
|
||||
<label class="btn btn-info" for="summary-default">(none)</label>
|
||||
<label class="btn btn-outline-info" for="summary-default">(none)</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="_summary" id="summary-true" value="true" />
|
||||
<label class="btn btn-info" for="summary-true">true</label>
|
||||
<label class="btn btn-outline-info" for="summary-true">true</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="_summary" id="summary-text" value="text" />
|
||||
<label class="btn btn-info" for="summary-text">text</label>
|
||||
<label class="btn btn-outline-info" for="summary-text">text</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="_summary" id="summary-data" value="data" />
|
||||
<label class="btn btn-info" for="summary-data">data</label>
|
||||
<label class="btn btn-outline-info" for="summary-data">data</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="_summary" id="summary-count" value="count" />
|
||||
<label class="btn btn-info" for="summary-count">count</label>
|
||||
<label class="btn btn-outline-info" for="summary-count">count</label>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" th:inline="javascript">
|
||||
@ -126,28 +126,30 @@
|
||||
|
||||
<h4>Server</h4>
|
||||
|
||||
<ul class="nav nav-sidebar">
|
||||
<li th:class="${page} == 'home' ? 'active' : ''">
|
||||
<a href="#" onclick="doAction(this, 'home', null);">Server Home/Actions</a>
|
||||
<ul class="nav flex-column nav-pills">
|
||||
<li class="nav-item">
|
||||
<a href="#" onclick="doAction(this, 'home', null);" class="nav-link" th:classappend="${page} == 'home' ? 'active' : ''">Server Home/Actions</a>
|
||||
</li>
|
||||
<li th:if="${supportsHfql}" th:class="${page} == 'hfql' ? 'active' : ''">
|
||||
<a href="#" id="leftHfql" onclick="doAction(this, 'hfql', null);">HFQL / SQL</a>
|
||||
<li th:if="${supportsHfql}" class="nav-item">
|
||||
<a href="#" id="leftHfql" onclick="doAction(this, 'hfql', null);" class="nav-link" th:classappend="${page} == 'hfql' ? 'active' : ''">HFQL / SQL</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>Resources</h4>
|
||||
|
||||
<ul class="nav nav-sidebar" th:unless="${conf.rest.empty}">
|
||||
<ul class="nav flex-column nav-pills" th:unless="${conf.rest.empty}">
|
||||
<th:block th:each="resource, resIterStat : ${conf.rest[0].resource}">
|
||||
<li th:class="${resourceName} == ${resource.typeElement.valueAsString} ? 'active' : ''">
|
||||
<li class="nav-item">
|
||||
|
||||
<a
|
||||
th:id="'leftResource' + ${resource.typeElement.valueAsString}"
|
||||
href="#"
|
||||
th:data1="${resource.typeElement.valueAsString}"
|
||||
class="nav-link"
|
||||
th:classappend="${resourceName} == ${resource.typeElement.valueAsString} ? 'active' : ''"
|
||||
onclick="doAction(this, 'resource', this.getAttribute('data1'));">
|
||||
<th:block th:text="${resource.typeElement.valueAsString}" >Patient</th:block>
|
||||
<span class="badge badge-secondary" th:if="${resourceCounts[resource.typeElement.valueAsString]} != null" th:text="${resourceCounts[resource.typeElement.valueAsString]}"/>
|
||||
<span class="badge text-bg-secondary" th:if="${resourceCounts[resource.typeElement.valueAsString]} != null" th:text="${resourceCounts[resource.typeElement.valueAsString]}"/>
|
||||
</a>
|
||||
</li>
|
||||
</th:block>
|
||||
|
@ -38,6 +38,9 @@ H3 {
|
||||
H4 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.sidebar > H4 {
|
||||
margin-top: 1.0em;
|
||||
}
|
||||
|
||||
.clientCodeBox {
|
||||
font-family: monospace;
|
||||
@ -220,53 +223,11 @@ body .syntaxhighlighter .line {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
top: 5px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
padding: 5px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar navigation */
|
||||
.nav-sidebar {
|
||||
margin-right: -21px; /* 20px padding + 1px border */
|
||||
margin-bottom: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.nav-sidebar > li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-sidebar > li > a {
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-sidebar > .active > a {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: #007bff !important;
|
||||
border: none !important;
|
||||
color: #FFF !important;
|
||||
font-weight: bold;
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Main content
|
||||
|
@ -61,7 +61,7 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
$('<input />', { id: 'param.' + theRowNum + '.0', placeholder: 'ResourceType/nnn', type: 'text', 'class': 'form-control' })
|
||||
)
|
||||
);
|
||||
} else if (theSearchParamType == 'token') {
|
||||
} else if (theSearchParamType === 'token') {
|
||||
|
||||
var tokenQualifiers = [];
|
||||
tokenQualifiers.push({});
|
||||
@ -115,31 +115,25 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
for (var i = 0; i < tokenQualifiers.length; i++) {
|
||||
var qualName = tokenQualifiers[i].name;
|
||||
var nextValue = tokenQualifiers[i].value;
|
||||
var nextLink = $('<a>' + tokenQualifiers[i].name+'</a>');
|
||||
tokenQualifierDropdown.append($('<li />').append(nextLink));
|
||||
var nextLink = $('<a class="dropdown-item">' + tokenQualifiers[i].name+'</a>');
|
||||
tokenQualifierDropdown.append($('<li/>').append(nextLink));
|
||||
nextLink.click(clickTokenFunction(nextValue, qualName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
$('<div />', { 'class':'input-group'}).append(
|
||||
$('<div />', {'class':'input-group-prepend'}).append(
|
||||
$('<button />', {'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown'}).append(
|
||||
tokenQualifierLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
tokenQualifierDropdown
|
||||
),
|
||||
$('<div />', { 'class':'input-group-prepend'} ).append(
|
||||
$('<div class="input-group-text">System</div>')
|
||||
),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.0', placeholder: "(opt)" }),
|
||||
$('<div />', { 'class':'input-group-prepend'} ).append(
|
||||
$('<div class="input-group-text">Code</div>')
|
||||
),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "(opt)" })
|
||||
)
|
||||
$('<div />', { 'class':'input-group'}).append(
|
||||
$('<button />', {'class':'btn btn-outline-primary', 'data-bs-toggle':'dropdown'}).append(
|
||||
tokenQualifierLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
tokenQualifierDropdown,
|
||||
$('<label class="input-group-text bg-light">System</label>'),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.0', placeholder: "(opt)" }),
|
||||
$('<label class="input-group-text bg-light">Code</label>'),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "(opt)" })
|
||||
),
|
||||
);
|
||||
|
||||
} else if (theSearchParamType === 'string') {
|
||||
@ -158,7 +152,7 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
);
|
||||
|
||||
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
|
||||
var qualifierDropdown = $('<div />', {'class':'dropdown-menu', role:'menu'});
|
||||
var qualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'});
|
||||
|
||||
function clickFunction(value, name){
|
||||
return function(){
|
||||
@ -176,13 +170,11 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
$('<div />', { 'class': 'input-group' }).append(
|
||||
$('<div />', {'class':'input-group-prepend btn-group'}).append(
|
||||
$('<button />', {'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown'}).append(
|
||||
matchesLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
qualifierDropdown
|
||||
$('<button />', {'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown'}).append(
|
||||
matchesLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
qualifierDropdown,
|
||||
$('<input />', { id: 'param.' + theRowNum + '.0', placeholder: placeholderText, type: 'text', 'class': 'form-control' })
|
||||
)
|
||||
);
|
||||
@ -299,9 +291,8 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
qualifierInput
|
||||
);
|
||||
|
||||
|
||||
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
|
||||
var qualifierDropdown = $('<div />', {'class': 'dropdown-menu', role: 'menu'});
|
||||
var qualifierDropdown = $('<ul />', {'class': 'dropdown-menu', role: 'menu'});
|
||||
|
||||
function clickFunction(value, name) {
|
||||
return function () {
|
||||
@ -310,22 +301,20 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < qualifiers.length; i++) {
|
||||
var nextLink = $('<a>' + qualifiers[i].name + '</a>');
|
||||
var nextLink = $('<a class="dropdown-item">' + qualifiers[i].name + '</a>');
|
||||
var qualName = qualifiers[i].name;
|
||||
var nextValue = qualifiers[i].value;
|
||||
qualifierDropdown.append($('<li />').append(nextLink));
|
||||
nextLink.click(clickFunction(nextValue, qualName));
|
||||
}
|
||||
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
$('<div />', {'class': 'input-group'}).append(
|
||||
$('<div />', {'class': 'input-group-prepend btn-group'}).append(
|
||||
$('<button />', {'class': 'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle': 'dropdown'}).append(
|
||||
matchesLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
qualifierDropdown
|
||||
),
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
$('<div />', {'class': 'input-group'}).append(
|
||||
$('<button />', {'class': 'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle': 'dropdown'}).append(
|
||||
matchesLabel,
|
||||
$('<span class="caret" style="margin-left: 5px;"></span>')
|
||||
),
|
||||
qualifierDropdown,
|
||||
$('<input />', {
|
||||
id: 'param.' + theRowNum + '.0',
|
||||
placeholder: placeholderText,
|
||||
@ -359,37 +348,22 @@ function addSearchControlDate(theSearchParamName, theContainerRowNum, theRowNum,
|
||||
} else {
|
||||
input = $('<div />', { 'class':'input-group date', 'data-bs-toggledate-format':'YYYY-MM-DDTHH:mm:ss' });
|
||||
}
|
||||
var qualifierDiv = $('<div />', {'class':'input-group-prepend'});
|
||||
|
||||
input.append(
|
||||
qualifierDiv,
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + inputId1 }),
|
||||
$('<div />', { 'class':'input-group-append input-group-addon'} ).append(
|
||||
$('<span />', {'class':'input-group-text'}).append(
|
||||
$('<i />', { 'class':'far fa-calendar-alt'})
|
||||
)
|
||||
)
|
||||
);
|
||||
input.datetimepicker({
|
||||
format: "YYYY-MM-DD",
|
||||
showTodayButton: true
|
||||
});
|
||||
// Set up the qualifier dropdown after we've initialized the datepicker, since it
|
||||
// overrides all addon buttons while it inits..
|
||||
qualifierDiv.addClass('input-group-btn');
|
||||
var qualifierTooltip = "Set a qualifier and a date to specify a boundary date. Set two qualifiers and dates to specify a range.";
|
||||
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('eq');
|
||||
|
||||
// Set up the qualifier dropdown after we've initialized the datepicker, since it
|
||||
// overrides all addon buttons while it inits..
|
||||
var qualifierTooltip = "Set a qualifier and a date to specify a boundary date. Set two qualifiers and dates to specify a range.";
|
||||
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('eq');
|
||||
qualifierBtn.tooltip({
|
||||
'selector': '',
|
||||
'placement': 'top',
|
||||
'container':'body'
|
||||
});
|
||||
var qualifierBtnEq = $('<a>eq</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'eq'); });
|
||||
var qualifierBtnGt = $('<a>gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
|
||||
var qualifierBtnGe = $('<a>ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
|
||||
var qualifierBtnLt = $('<a>lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
|
||||
var qualifierBtnLe = $('<a>le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
|
||||
qualifierDiv.append(
|
||||
var qualifierBtnEq = $('<a class="dropdown-item">eq</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'eq'); });
|
||||
var qualifierBtnGt = $('<a class="dropdown-item">gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
|
||||
var qualifierBtnGe = $('<a class="dropdown-item">ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
|
||||
var qualifierBtnLt = $('<a class="dropdown-item">lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
|
||||
var qualifierBtnLe = $('<a class="dropdown-item">le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
|
||||
input.append(
|
||||
qualifierBtn,
|
||||
$('<ul class="dropdown-menu" role="menu">').append(
|
||||
$('<li />').append(qualifierBtnEq),
|
||||
@ -400,46 +374,50 @@ function addSearchControlDate(theSearchParamName, theContainerRowNum, theRowNum,
|
||||
)
|
||||
);
|
||||
|
||||
var dateTimePicker = $('<input />', { type:'text', 'class':'form-control', id: 'param.' + inputId1 });
|
||||
input.append(
|
||||
dateTimePicker
|
||||
);
|
||||
|
||||
dateTimePicker.datetimepicker({
|
||||
format: "YYYY-MM-DD",
|
||||
showTodayButton: true
|
||||
});
|
||||
|
||||
input.append(
|
||||
$('<span />', {'class':'input-group-text'}).append(
|
||||
$('<i />', { 'class':'far fa-calendar-alt'})
|
||||
)
|
||||
);
|
||||
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
qualifier,
|
||||
$('<div />', { }).append(
|
||||
input
|
||||
)
|
||||
input
|
||||
);
|
||||
}
|
||||
|
||||
function addSearchControlQuantity(theSearchParamName, theContainerRowNum, theRowNum) {
|
||||
var input = $('<div />', { 'class':'input-group'});
|
||||
var qualifier = $('<input />', {type:'hidden', id:'param.' + theRowNum + '.0'});
|
||||
var qualifierDiv = $('<div />', {'class':'input-group-prepend'});
|
||||
|
||||
input.append(
|
||||
qualifierDiv,
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "value" }),
|
||||
$('<div />', { 'class':'input-group-append'} ).append(
|
||||
$('<span class="input-group-text">System</span>')
|
||||
),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.2', placeholder: "(opt)" }),
|
||||
$('<div />', { 'class':'input-group-append'} ).append(
|
||||
$('<span class="input-group-text">Code</span>')
|
||||
),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.3', placeholder: "(opt)" })
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
qualifier,
|
||||
input
|
||||
);
|
||||
|
||||
var qualifierTooltip = "You can optionally use a qualifier to specify a range.";
|
||||
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('=');
|
||||
qualifierBtn.tooltip({
|
||||
'selector': '',
|
||||
'placement': 'left',
|
||||
'container':'body'
|
||||
});
|
||||
var qualifierBtnEq = $('<a>=</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, '='); });
|
||||
var qualifierBtnAp = $('<a>ap (Approx)</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ap'); });
|
||||
var qualifierBtnGt = $('<a>gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
|
||||
var qualifierBtnGe = $('<a>ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
|
||||
var qualifierBtnLt = $('<a>lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
|
||||
var qualifierBtnLe = $('<a>le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
|
||||
qualifierDiv.append(
|
||||
var qualifierTooltip = "You can optionally use a qualifier to specify a range.";
|
||||
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('=');
|
||||
qualifierBtn.tooltip({
|
||||
'selector': '',
|
||||
'placement': 'left',
|
||||
'container':'body'
|
||||
});
|
||||
var qualifierBtnEq = $('<a class="dropdown-item">=</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, '='); });
|
||||
var qualifierBtnAp = $('<a class="dropdown-item">ap (Approx)</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ap'); });
|
||||
var qualifierBtnGt = $('<a class="dropdown-item">gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
|
||||
var qualifierBtnGe = $('<a class="dropdown-item">ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
|
||||
var qualifierBtnLt = $('<a class="dropdown-item">lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
|
||||
var qualifierBtnLe = $('<a class="dropdown-item">le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
|
||||
input.append(
|
||||
qualifierBtn,
|
||||
$('<ul class="dropdown-menu" role="menu">').append(
|
||||
$('<li />').append(qualifierBtnEq),
|
||||
@ -451,12 +429,14 @@ function addSearchControlQuantity(theSearchParamName, theContainerRowNum, theRow
|
||||
)
|
||||
);
|
||||
|
||||
$('#search-param-rowopts-' + theContainerRowNum).append(
|
||||
qualifier,
|
||||
$('<div />', { }).append(
|
||||
input
|
||||
)
|
||||
input.append(
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "value" }),
|
||||
$('<label class="input-group-text">System</label>'),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.2', placeholder: "(opt)" }),
|
||||
$('<label class="input-group-text">Code</label>'),
|
||||
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.3', placeholder: "(opt)" })
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function handleSearchParamTypeChange(select, params, theContainerRowNum, theParamRowNum) {
|
||||
|
@ -16,6 +16,7 @@ import ca.uhn.fhir.sl.cache.Cache;
|
||||
import ca.uhn.fhir.sl.cache.CacheFactory;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import ca.uhn.fhir.util.Logs;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import jakarta.annotation.Nonnull;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@ -955,9 +956,19 @@ public class ValidationSupportChain implements IValidationSupport {
|
||||
|
||||
return returnValue;
|
||||
} else {
|
||||
retVal = new CacheValue<>(theLoader.get());
|
||||
myNonExpiringCache.put(theKey, retVal);
|
||||
putInCache(theKey, retVal);
|
||||
// Avoid flooding the validation support modules tons of concurrent
|
||||
// requests for the same thing
|
||||
synchronized (this) {
|
||||
retVal = getFromCache(theKey);
|
||||
if (retVal == null) {
|
||||
StopWatch sw = new StopWatch();
|
||||
ourLog.info("Performing initial retrieval for non-expiring cache: {}", theKey);
|
||||
retVal = new CacheValue<>(theLoader.get());
|
||||
ourLog.info("Initial retrieval for non-expiring cache {} succeeded in {}", theKey, sw);
|
||||
myNonExpiringCache.put(theKey, retVal);
|
||||
putInCache(theKey, retVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user