Skip to content

Commit bb28205

Browse files
namedgraphclaude
andauthored
Document tabs (#294)
* Add external document tabs and fix related CSS/XPath regressions (#293) - Render proxied external documents in tab panes (bs2:TabBody/ldh:CreateTab/ldh:ActivateTab/ldh:RenderTabPane) - Convert breadcrumb-nav from id to CSS class to support per-pane breadcrumbs; populate breadcrumbs in external tab panes; extract ldh:PopulateBreadcrumbNav named template to DRY up local/external breadcrumb logic - Fix CSS block border selectors: #content-body → .content-body, add intermediate container-fluid level - Narrow btn-edit and btn-create-chart match patterns to require .block ancestor, preventing false matches on action-bar - Fix drag handler match patterns: update stale direct-child path to match new two-level content-body/container-fluid/block structure - Fix modal content-body lookup: use ancestor:: for action-bar buttons, active tab-pane for global buttons, so modals append to the visible pane for proxied documents - Remove dead document-mode-tabs template and broken progress bar update targeting hidden #content-body - Remove unused getModes() override from XSLTWriterBase Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix disabled edit button triggering modal and move action buttons inside auth guard - Prevent click handler from firing on disabled edit button by adding [not(contains-token(@Class, 'disabled'))] predicate - Move ACL and Edit buttons inside the xsl:if auth guard so they are not rendered for unauthorized users - Wrap edit button in pull-right div and add ldh:logo icon to match ACL button structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Ignore clicks on disabled edit button in modal click handler Add [not(contains-token(@Class, 'disabled'))] predicate to the btn-edit match template so that clicking a disabled edit button does not open the edit modal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Refactor external doc tabs: per-URI panes, data-* state, tab-pane-id param - Replace LinkedDataHub.tabs/tab-counter JS registry with data-uri/data-endpoint attributes directly on tab <li> elements; sd:endpoint() now reads from dataset - Give each external document its own tab pane (one per URI) instead of a shared external pane; tab pane visibility toggled via display:none/block - Rename ldh:CreateTab → ldh:AddTabNavBarListItem (no longer handles rendering); ldh:RenderTab now accepts $tab-pane-id (xs:string) instead of $tab-pane element, since the element is detached after ixsl:replace-element - ProxyRequestFilter: inject MediaTypes, throw NotAcceptableException on no variant match instead of silently returning; remove manual HTML Accept-header inspection - Remove $ac:uri XSLT param and related proxy-target plumbing from XSLTWriterBase and layout.xsl; remove LDH.access_to vocabulary property - Switch service document fetches from ac:build-uri() to ldh:href() throughout - Move srx:sparql bs2:ContentBody template from layout.xsl to document.xsl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Simplified tab rendering * (X)HTML response fix in the proxy filter * Additional tests HTTP test and unit test * Restored (X)HTML workaround in the proxy filter * Add margin-top to .content-body to restore spacing after sticky action-bar change The action-bar moved from inside .navbar-fixed-top (120px body padding) to sticky positioning (55px body padding), eliminating the 14px gap that existed between the action-bar and content-body. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Simplify external doc tab rendering and add lapp:application Link header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove services fetch, service dropdown, and refactor navbar into separate templates The client-side getResourceWithRetry(servicesRequestUri) fetch and ldh:apps stylesheet parameter were unused — remove them along with the service select dropdown and related onServiceLoad/RenderServices templates. Refactor the navbar into bs2:NavBarLeft, bs2:NavBarMain, and bs2:NavBarRight templates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Tab activation fixes * Hide system resources in client-side XSLT * Fix navbar/tab-bar/action-bar spacing and move sidebar into tab panes Update hardcoded 55px navbar height to 51px across CSS and XSLT to match actual rendered height after search bar refactoring. Move left-sidebar from global #left-sidebar to per-tab-pane .left-sidebar, store endpoint/base/application as data attributes on tab panes instead of tab-bar list items, and refactor navigation to scope sidebar operations to the active tab pane. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add document-body wrapper, introduce ldh:DocumentNavigate, fix content-body XPaths Wraps action-bar and content-body in a document-body div (carrying @about/@typeof), removes XHTML-based ldh:xhtml-document-loaded in favour of a new ldh:DocumentNavigate named template that centralises local/external navigation via RDF. Fixes content-body ancestor XPaths broken by the new document-body wrapper, pre-renders the local tab in the tab-bar, and removes server-side SD endpoint / forShape param passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix modal document edit error by closing modal and reloading document on PATCH success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Move abort controller and cursor into ldh:DocumentNavigate, simplify call sites Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * `bs2:DocumentBody` mode * Render signup form client-side, implement tab close - Move signup.xsl include from admin/layout.xsl to client.xsl so its templates compile into the Saxon-JS SEF; drop use-when="SAXON" gates, switch $ldt:base to ldt:base() and $main-doc to ldh:base-uri(.) - Replace ldh:construct / Construct.java extension function with ldh:construct-forClass via the existing /ns endpoint - Implement tab close: remove pane, activate fallback tab, hide tab-bar when only the base-uri tab remains; reposition .tab-close as absolute corner element - Drop dead error-alert fallback in ldh:rdf-document-response Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fixed relative URL * `ldh:NavigationUpdate` call moved into `ldh:RenderTab` * Request URI fixes * Pipe proxied HTTP responses as InputStream instead of parsing/re-serializing Pre-selecting a variant from a combined Model+ResultSet writable types list before knowing the response entity type caused a 500 when the upstream returned RDF (read as Model) but the pre-selected variant was application/sparql-results+xml — Jersey had no MessageBodyWriter<Model> for that type. External HTTP responses are now forwarded verbatim (status, Content-Type, body, Link headers). The upstream already negotiates content type via the forwarded Accept header. Local paths (DataManager cache, ontology DESCRIBE) keep proper Model-specific content negotiation. Also removes the dead BadGatewayExceptionMapper that incorrectly mapped BadGatewayException to 500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove stale testNullVariantThrowsNotAcceptable test The filter no longer pre-selects a variant, so 406 for unsatisfiable Accept is delegated to the upstream. The test and its now-unused getSupportedLanguages() stub are removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fixed client's accepted media types * Proxied `ETag` header test * Proxied HTML `Accept` test * Forward end-to-end response headers from proxied upstream * Use top-quality `Accept` value to decide HTML proxy bypass * Wrap XSLT `document()` URIs with `ldh:href` and clean up client templates * Rework proxy `Accept` regression tests, simplify q-rank bypass ProxyRequestFilter: - Drop the `quality(MediaType)` helper and the topQ/filter pipeline. Per the JAX-RS spec, `getAcceptableMediaTypes()` is sorted by q descending (Jersey: `HttpHeaderReader.readQualifiedList` → `AcceptableMediaType.COMPARATOR`), so the first non-wildcard type is the top-ranked one. Replace with a single loop that returns on (X)HTML and breaks otherwise. Tests: - Add `GET-proxied-accept-forwarded.sh` regressing 708edfc: send a single specific RDF type as `Accept` and assert the response `Content-Type` matches. Pre-fix, ProxyRequestFilter substituted its own readable-types list for the client's `Accept`, so upstream could pick any RDF format (e.g. rdf-thrift). - Rework `GET-proxied-accept-html-not-preferred.sh` to use HTTP status as the bypass-vs-forward discriminator. The previous content-type assertion was unreachable in a symmetric admin/end-user setup — both paths negotiate the same media type. A UUID-named non-existent path produces 200 on bypass (`ApplicationFilter` strips `?uri=` → admin root) and 404 on forward. - Silence `DEBUG:` echoes in `HEAD-proxied-etag.sh` to match the convention of the other proxy tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Removed debug messages * Removed unused template * Revert "Removed unused template" This reverts commit ec76099. * Wire `bs2:SignUpComplete` into signup success path - Pass `'form-signup'` to `bs2:RowForm` via `form-id` (lands on the form itself) instead of `id` (wrapper div), so the new submit handler matches. - Add `match="form[@id = 'form-signup']"` template that overrides `$callback` via `xsl:next-match`, plus `ldh:signup-form-response`: `201 + Location` calls `bs2:SignUpComplete`, otherwise delegates to `ldh:row-form-response`. - Update `bs2:SignUpComplete` to target the active tab pane's `content-body` (post-tabs DOM no longer has a singleton `#content-body`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 73d540f commit bb28205

35 files changed

Lines changed: 1407 additions & 1762 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the readers group to be able to read documents
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/readers/"
17+
18+
# Regression: ProxyRequestFilter must forward the client's Accept header verbatim to the
19+
# upstream, NOT substitute its own readable-types list. Previously the filter built its
20+
# outbound Accept from MediaTypes.getReadable(Model.class) + getReadable(ResultSet.class)
21+
# (everything Jena could ingest, all q=1.0), discarding what the client actually asked for.
22+
# The upstream then content-negotiated against that broad list and could legally pick any
23+
# RDF format — e.g. application/rdf+thrift — even when the client (e.g. SaxonJS document())
24+
# explicitly requested application/rdf+xml or application/xml.
25+
#
26+
# Verify by requesting one specific RDF type and asserting the response matches it.
27+
28+
for accept in 'application/rdf+xml' 'text/turtle' 'application/n-triples'; do
29+
content_type=$(curl -k -f -s -G -w "%{content_type}" -o /dev/null \
30+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
31+
-H "Accept: $accept" \
32+
--data-urlencode "uri=${END_USER_BASE_URL}" \
33+
"$ADMIN_BASE_URL")
34+
35+
case "$content_type" in
36+
"$accept"*) ;;
37+
*) exit 1 ;;
38+
esac
39+
done
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the readers group to be able to read documents
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/readers/"
17+
18+
# Regression: when a client lists application/xhtml+xml (or text/html) in Accept at a
19+
# LOWER q-value than another supported type, the proxy must treat the request as
20+
# API-client intent and forward — not as browser navigation that wants the app shell.
21+
# Previously, ProxyRequestFilter bypassed on anyMatch(HTML or XHTML in Accept) without
22+
# checking q-rank, so it false-fired on any Accept that mentioned HTML at all and
23+
# returned the local app shell instead of the proxied response.
24+
#
25+
# Discriminator is HTTP status — content-type cannot tell bypass from forward because
26+
# admin and end-user share writer configs (same Accept → same negotiated type on both).
27+
# A UUID-named path that doesn't exist on either origin disambiguates:
28+
# - bypass: ApplicationFilter strips ?uri= → request URI becomes admin root → 200
29+
# - forward: proxy forwards the actual UUID path to end-user → 404
30+
31+
accept_header='application/xml, text/xml;q=0.9, application/xhtml+xml;q=0.8, */*;q=0.7'
32+
non_existing_uri="${END_USER_BASE_URL}$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)/"
33+
34+
status=$(curl -k -s -G -o /dev/null -w "%{http_code}" \
35+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
36+
-H "Accept: $accept_header" \
37+
--data-urlencode "uri=${non_existing_uri}" \
38+
"$ADMIN_BASE_URL")
39+
40+
[ "$status" = "$STATUS_NOT_FOUND" ] || exit 1

http-tests/proxy/GET-proxied-external-502.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add-agent-to-group.sh \
1919

2020
curl -k -w "%{http_code}\n" -o /dev/null -s \
2121
-G \
22+
-H "Accept: application/n-triples" \
2223
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
2324
--data-urlencode "uri=http://f1d2d4cf-90bb-4f5b-ae4b-921e584b6edd.org" \
2425
"$END_USER_BASE_URL" \
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
initialize_dataset "$END_USER_BASE_URL" "$TMP_END_USER_DATASET" "$END_USER_ENDPOINT_URL"
5+
initialize_dataset "$ADMIN_BASE_URL" "$TMP_ADMIN_DATASET" "$ADMIN_ENDPOINT_URL"
6+
purge_cache "$END_USER_VARNISH_SERVICE"
7+
purge_cache "$ADMIN_VARNISH_SERVICE"
8+
purge_cache "$FRONTEND_VARNISH_SERVICE"
9+
10+
# add agent to the readers group to be able to read documents
11+
12+
add-agent-to-group.sh \
13+
-f "$OWNER_CERT_FILE" \
14+
-p "$OWNER_CERT_PWD" \
15+
--agent "$AGENT_URI" \
16+
"${ADMIN_BASE_URL}acl/groups/readers/"
17+
18+
extract_etag() {
19+
grep -i '^etag:' \
20+
| tr -d '\r' \
21+
| sed 's/^[Ee][Tt][Aa][Gg]:[[:space:]]*//'
22+
}
23+
24+
# fetch the end-user root directly to capture its ETag
25+
26+
direct_etag=$(curl --head -k -f -s \
27+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
28+
-H 'Accept: application/n-triples' \
29+
"$END_USER_BASE_URL" \
30+
| extract_etag)
31+
32+
# fetch the same document via the admin proxy
33+
34+
proxied_etag=$(curl -G --head -k -f -s \
35+
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
36+
-H 'Accept: application/n-triples' \
37+
--data-urlencode "uri=${END_USER_BASE_URL}" \
38+
"$ADMIN_BASE_URL" \
39+
| extract_etag)
40+
41+
[ -n "$proxied_etag" ] || exit 1
42+
[ "$proxied_etag" = "$direct_etag" ] || exit 1

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,6 @@ protected PasswordAuthentication getPasswordAuthentication()
842842
xsltProc.registerExtensionFunction(new UUID());
843843
xsltProc.registerExtensionFunction(new DecodeURI());
844844
xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.URLDecode());
845-
xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.Construct(xsltProc));
846845
xsltProc.registerExtensionFunction(new com.atomgraph.linkeddatahub.writer.function.SendHTTPRequest(xsltProc, client));
847846

848847
Model mappingModel = locationMapper.toModel();

0 commit comments

Comments
 (0)