Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b20c3f3507 | ||
|
|
f803656936 | ||
|
|
5d9a133261 | ||
|
|
6af8d77d80 | ||
|
|
8839ebd8ce | ||
|
|
3435c17c26 | ||
|
|
1587530d9c | ||
|
|
6af7692f97 | ||
|
|
6367ed2f43 | ||
|
|
91c49a27c5 | ||
|
|
24e088d663 | ||
|
|
d4e7c26f13 | ||
|
|
4623bfa5e2 | ||
|
|
73222aa9bd | ||
|
|
17bdecff89 | ||
|
|
75a97e6300 | ||
|
|
b88002a752 | ||
|
|
c5b5acbcbc | ||
|
|
982cf5b28e | ||
|
|
41a8961905 | ||
|
|
9fd1d1ef65 | ||
|
|
76b8c5fb07 | ||
|
|
ca62bb391c | ||
|
|
856e57173f | ||
|
|
c29c955e1a | ||
|
|
c7626b2797 | ||
|
|
f08c2dff0e | ||
|
|
de47b25e11 | ||
|
|
561e92e090 | ||
|
|
971b007606 | ||
|
|
05daf64ba6 | ||
|
|
2076eddd1b | ||
|
|
94bf510010 | ||
|
|
d6065826fb | ||
|
|
e9d48594d7 | ||
|
|
64c3fb51aa | ||
|
|
3e4e741f75 | ||
|
|
c25686c77b | ||
|
|
57edfe5478 | ||
|
|
a70daf1789 | ||
|
|
ac48b91bf4 | ||
|
|
617007bbbd | ||
|
|
d96453ad42 | ||
|
|
cddf39e58c | ||
|
|
710463ce06 | ||
|
|
926299f2ee | ||
|
|
98e41dbbd7 | ||
|
|
3d5090d49d | ||
|
|
9d27f37ad2 | ||
|
|
b9f7300708 | ||
|
|
e233f508b6 | ||
|
|
42ffc63009 | ||
|
|
2272f55162 | ||
|
|
183037c4d3 | ||
|
|
b7794cef47 | ||
|
|
2b60c7443b | ||
|
|
bef7a440ac | ||
|
|
21fcf2d6fa | ||
|
|
127f10fba9 | ||
|
|
8a22e28a46 | ||
|
|
8ed0f16004 | ||
|
|
cc2441fd2d | ||
|
|
9d48240111 | ||
|
|
0de0fbde4a | ||
|
|
10ca9547b3 | ||
|
|
cc91f8b1f9 | ||
|
|
c5d6806d53 | ||
|
|
b1ec8bec7c | ||
|
|
029eee6c7b | ||
|
|
e995626ff4 |
No files matched your search
@@ -3,6 +3,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# One deploy at a time: `surfer deploy` swaps the site atomically, so two runs
|
||||
# racing is not corruption, but it is a wasted build and the last swap wins.
|
||||
concurrency:
|
||||
group: surfer-docs
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -19,7 +26,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12' # Specify your desired version (e.g., 3.10 or 3.12)
|
||||
python-version: '3.12' # Specify your desired version (e.g. 3.10 or 3.12)
|
||||
env:
|
||||
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
|
||||
RUNNER_TOOL_CACHE: /opt/hostedtoolcache
|
||||
@@ -31,19 +38,39 @@ jobs:
|
||||
apt install -y ./google-chrome*.deb;
|
||||
export CHROME_BIN=/usr/bin/google-chrome
|
||||
|
||||
- name: Install Surfer
|
||||
run: |
|
||||
npm -g install cloudron-surfer
|
||||
|
||||
- name: build site
|
||||
run: |
|
||||
pip install mkdocs
|
||||
pip install mkdocs-material
|
||||
pip install fontawesome_markdown
|
||||
pip install mkdocs-redirects
|
||||
pip install mkdocs==1.6.1
|
||||
pip install mkdocs-material==9.7.7
|
||||
pip install fontawesome_markdown==0.2.6
|
||||
pip install mkdocs-redirects==1.2.3
|
||||
mkdocs build --clean
|
||||
mkdir site/slides
|
||||
npx @marp-team/marp-cli slides/innovation/innovation.md -o ./site/slides/innovation/index.html
|
||||
npx @marp-team/marp-cli slides/if/if.md -o site/if.pdf --allow-local-files --html=true
|
||||
npx @marp-team/marp-cli@4.5.1 slides/innovation/innovation.md -o ./site/slides/innovation/index.html
|
||||
npx @marp-team/marp-cli@4.5.1 slides/if/if.md -o site/if.pdf --allow-local-files --html=true
|
||||
|
||||
# Deploy tooling is pinned to exact versions: the workflow builds whatever
|
||||
# is committed, and an unpinned `latest` would let a breaking or
|
||||
# compromised upstream release ship straight to the live site.
|
||||
#
|
||||
# `deploy` (v7) replaces the site atomically - it extracts to a staging
|
||||
# folder and swaps it in, so a failed upload leaves the current site
|
||||
# serving. The v6 `put` this replaces was token authenticated, and v7
|
||||
# takes the Cloudron username plus an app password instead.
|
||||
#
|
||||
# The docs live in a NAMED site, `docs`, served at its own alias domain -
|
||||
# not the app's default site - so the name and the domain are both passed.
|
||||
- name: Deploy docs
|
||||
run: surfer put --token ${{ secrets.SURFERKEY }} --server docs.datacontroller.io site/* /
|
||||
env:
|
||||
SURFER_PASSWORD: ${{ secrets.SURFER_PASSWORD }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npx --yes @cloudron/surfer@7.2.0 deploy site \
|
||||
--server docs.datacontroller.io \
|
||||
--username allan \
|
||||
--password "$SURFER_PASSWORD" \
|
||||
--site-name docs \
|
||||
--domain docs.datacontroller.io \
|
||||
--message "Build from ${REF_NAME} @ ${COMMIT_SHA:0:8}"
|
||||
@@ -0,0 +1,11 @@
|
||||
# npm configuration for this repo's CI and local installs.
|
||||
#
|
||||
# ignore-scripts keeps package install scripts (an arbitrary-code execution
|
||||
# point) from running during the deploy workflow. Both npm packages the
|
||||
# workflow installs - cloudron-surfer and @marp-team/marp-cli - ship no
|
||||
# install, preinstall or postinstall hook, so nothing is skipped.
|
||||
#
|
||||
# save-exact pins anything installed from here to an exact version rather than
|
||||
# a caret range.
|
||||
ignore-scripts=true
|
||||
save-exact=true
|
||||
@@ -0,0 +1,11 @@
|
||||
# Hooks for the pre-commit framework - https://pre-commit.com
|
||||
#
|
||||
# This repo carries no native git hooks, so this is the route for a secret scan
|
||||
# at commit time: install the framework once with `pre-commit install`, and the
|
||||
# scan then runs on every commit. The hook is pinned to an exact gitleaks
|
||||
# release tag.
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
@@ -35,7 +35,9 @@ When importing an excel file, all rows are loaded into the web page. You have a
|
||||
A CSV upload bypasses the part where the records are loaded into the web page, and ALL rows are sent to the staging area directly. This makes it suitable for larger uploads.
|
||||
|
||||
## Edit Stage Approve Workflow
|
||||
Up to 500 rows can be edited (in the web editor) at one time. These edits are submitted to a staging area. After one or more approvals (acceptances) the changes are applied to the source table.
|
||||
The EDIT screen loads up to [DC_MAXOBS_WEBEDIT](/dcc-options/#dc_maxobs_webedit) rows (250 by default), or as many rows as fit within [DC_MAXCELLS_WEBEDIT](/dcc-options/#dc_maxcells_webedit) (200,000 cells) - whichever is reached first, so a wide table loads fewer rows than a narrow one. A larger table is not refused: it loads truncated, with a warning against the table name, and the filter is the way to reach the rest.
|
||||
|
||||
These edits are submitted to a staging area. After one or more approvals (acceptances) the changes are applied to the source table.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ The Editor screen lets users who have been pre-authorised (via the `DATACTRL.MPE
|
||||
|
||||
2 - *Upload*. If you have a lot of data, you can [upload it directly](files). The changes are then approved in the usual way.
|
||||
|
||||
3 - *Edit*. This is the main interface, data is displayed in tabular format. The first column is always "Delete?", as this allows you to mark rows for deletion. Note that removing a row from display does not mark it for deletion! It simply means that this row is not part of the changeset being submitted.
|
||||
3 - *Edit*. This is the main interface, data is displayed in tabular format. The first column is always "Delete?", as this allows you to mark rows for deletion. This column is created by Data Controller and is not part of your table. Note that removing a row from display does not mark it for deletion! It simply means that this row is not part of the changeset being submitted.
|
||||
The next set of columns are the Primary Key, and are shaded grey. If the table has a surrogate / retained key, then it is the Business Key that is shown here (the RK field is calculated / updated at the backend). For SCD2 type tables, the 'validity' fields are not shown. It is assumed that the user is always working with the current version of the data, and the view is filtered as such.
|
||||
After this, remaining columns are shown. Dates / datetime fields have appropriate datepickers. Other fields may also have dropdowns to ensure entry of standard values, these can be configured in the `DATACTRL.MPE_SELECTBOX` table.
|
||||
|
||||
|
||||
@@ -20,16 +20,25 @@ Currently used scopes include:
|
||||
### DC_EMAIL_ALERTS
|
||||
Set to YES or NO to enable email alerts. This requires email options to be preconfigured (mail server etc).
|
||||
|
||||
### DC_MAXCELLS_WEBEDIT
|
||||
By default, a maximum of 200,000 cells (observations multiplied by variables) can be edited in the browser at one time. This limit is applied together with [DC_MAXOBS_WEBEDIT](#dc_maxobs_webedit) - whichever is reached first.
|
||||
|
||||
A selection over either limit is served truncated: the grid carries the rows that fit, and a warning triangle next to the table name names the limit that was reached. Filter the table to see the rest.
|
||||
|
||||
The cell count matters because the cost of an EDIT screen load tracks the number of cells, not the number of rows. A 10 column table is comfortable at 250 rows, but 250 rows of a 1000 column table is 250,000 cells.
|
||||
|
||||
### DC_MAXOBS_WEBEDIT
|
||||
By default, a maximum of 100 observations can be edited in the browser at one time. This number can be increased, but note that the following factors will impact performance:
|
||||
By default, a maximum of 250 observations can be edited in the browser at one time. This number can be increased, but note that the following factors will impact performance:
|
||||
|
||||
* Number of configured [Validations](/dcc-validations)
|
||||
* Browser type and version (works best in Chrome)
|
||||
* Number (and size) of columns
|
||||
* Speed of client machine (laptop/desktop)
|
||||
|
||||
A wide table can reach the [DC_MAXCELLS_WEBEDIT](#dc_maxcells_webedit) limit well before this one, so increase both together.
|
||||
|
||||
### DC_MAXOBS_WEBVIEW
|
||||
By default, a maximum of 500 observations can be viewed in the browser at one time. Please see previous section for items to consider if increasing this value.
|
||||
By default, a maximum of 2000 observations can be viewed in the browser at one time. Please see previous section for items to consider if increasing this value.
|
||||
|
||||
### DC_REQUEST_LOGS
|
||||
On SASjs Server and SAS9 Server types, at the end of each DC SAS request, a record is added to the [MPE_REQUESTS](/tables/mpe_requests) table. In some situations this can cause table locks. To prevent this issue from occuring, the `DC_REQUEST_LOGS` option can be set to `NO` (Default is `YES`).
|
||||
|
||||
@@ -16,6 +16,9 @@ Adding tables to the Data Controller is a matter of configuration, specifically
|
||||
|
||||

|
||||
|
||||
!!! note
|
||||
`_____DELETE__THIS__RECORD_____` is a reserved name. The editor creates that column itself - it is the **Delete?** column - and never reads it from the table, so a target table must not contain a column of that name. The editor refuses to open a table that does, rather than let the two collide.
|
||||
|
||||
## MPE_TABLES Configuration Details
|
||||
|
||||
Each table to be edited in the Data Controller is represented by one record in `DATACTRL.MPE_TABLES`. The fields should be populated as follows:
|
||||
@@ -214,6 +217,8 @@ The code is simply `%include`'d at the relevant point during backend execution.
|
||||
|
||||
If the entry ends in `".sas"` it is assumed to be a physical, filesystem file. Otherwise, the source code is extracted from SAS Drive or Metadata.
|
||||
|
||||
The hook shares the WORK library of the service that runs it, so it can read whatever that service received. In particular the two browser tables are there whenever the app made the request - `work.browser_info` (the page URL, the embedding document, timezone, locale, versions) and `work.browser_url_vars` (the page's URL parameters, one row each). See [Browser Variables](/developer-guide/browser-variables/). A `POST_EDIT_HOOK` that runs on an uploaded file is the exception: the upload path is multipart and carries no input tables.
|
||||
|
||||
To illustrate:
|
||||
|
||||
* Physical filesystem (ends in .sas): `/opt/sas/code/myprogram.sas`
|
||||
|
||||
@@ -142,97 +142,24 @@ The app version is bundled into the frontend during the release, and is visible
|
||||
|
||||
You can also determine the app version (and SASjs Version, and build time) by opening browser Development Tools and running `appinfo()` in the console.
|
||||
|
||||
## The app does not start - Show log
|
||||
|
||||
A startup failure shows a modal with the error and the service that failed. When that service is `public/startupservice`, the modal offers **Show log**, which opens the service in a new browser window with `_debug=131`, so the SAS log is the page body.
|
||||
|
||||
The button exists because of how Viya runs a request from JavaScript: it can go through a Compute execution task (`_executionTasks=true`), where the log is not returned and the request can time out before the service finishes. The browser request runs the same stored program without a task, so the log is available even when the adapter got nothing back. On SAS 9 it opens the stored process, and on SASjs Server the route returns the service output rather than a log.
|
||||
|
||||
## Support Diagnostics
|
||||
|
||||
The frontend sends two input tables with the startup service and with the services that execute customer-provided code - the [hook scripts](macros.md) (pre/post edit and approve hooks) and the dynamic cell dropdown programs. They are available in those services as `work.browser_info` and `work.browser_url_vars`, so a support ticket can be diagnosed from the job log without asking the user follow-up questions, and a hook can adapt to its context. Other services do not receive them.
|
||||
|
||||
The services that receive the tables are: `public/startupservice`, `editors/getdata`, `editors/getdynamiccolvals`, `editors/stagedata`, `editors/loadfile`, `editors/restore` and `auditors/postdata`.
|
||||
|
||||
`editors/loadfile` is the exception in practice: the app reaches it through the adapter's file-upload call (multipart), which carries no input tables, so the service - and the post edit hook it runs - does not receive them in normal use. It is on the list for completeness, and would receive them if the service were ever invoked through the ordinary request path.
|
||||
The frontend sends two input tables with the startup service and with the services that execute customer-provided code - the [hook scripts](dcc-tables.md#hook-scripts) (pre/post edit and approve hooks) and the dynamic cell dropdown programs. See [Browser Variables](/developer-guide/browser-variables/) for the column lists, the log lines, how to read them from a hook, and which services receive them.
|
||||
|
||||
### browser_info
|
||||
|
||||
A single-row table with these columns:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it. When the editor is embedded in a report, the embedding URL is visible here. |
|
||||
| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL, so the embedding report can be told apart from the editor URL. |
|
||||
| `timezone` | The browser's IANA timezone name, e.g. `Europe/Berlin`. |
|
||||
| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it (positive west of UTC, so `Europe/Berlin` in summer is -120). |
|
||||
| `locale` | The browser locale, e.g. `en-GB`. |
|
||||
| `dc_version` | The Data Controller build, as shown by `appinfo()`. |
|
||||
| `adapter_version` | The `@sasjs/adapter` version the client was built with. |
|
||||
| `browser` | The browser family, parsed from the user agent. |
|
||||
| `browser_version` | The browser version. |
|
||||
| `platform` | The operating system, parsed from the user agent. |
|
||||
| `user_agent` | The raw `navigator.userAgent` string. |
|
||||
The browser context: page URL, embedding document, timezone, locale, versions, and the user agent. Columns and examples: [Browser Variables](/developer-guide/browser-variables/#browser_info).
|
||||
|
||||
### browser_url_vars
|
||||
|
||||
The page URL parameters as one row per parameter, so a SAS program can read a parameter by name rather than parse the `url` string:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| `name` | The URL parameter name. |
|
||||
| `value` | The URL parameter value. |
|
||||
|
||||
Parameters from both the search string and the hash query string (Angular routes carry them after the `#`) are included; on a name collision the search string value wins. The table is sent only when the page URL has at least one parameter.
|
||||
|
||||
On SAS 9 and Viya the app is reached through the platform's own job URL, so the platform's parameters (`_FILE`, `_program`, `_debug`, `_webout` and friends) are in the table alongside DC's. Read a parameter by name rather than assuming every row belongs to Data Controller - `embed` and `labels` are DC's, `_FILE` and `_program` are the platform's.
|
||||
|
||||
The values in both tables are supplied by the client, so treat them as diagnostics hints rather than a security boundary.
|
||||
|
||||
To see the tables, run any of the services above with debug on. The debug value depends on the path: `&_debug=131` on the Compute API and SAS 9, or `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled - which is what the frontend sends there, so turning debug on in the app is enough. Either value makes the session initialisation write the tables to the job log:
|
||||
|
||||
```
|
||||
NOTE: MPEINIT: work.browser_url_vars:
|
||||
name=embed value=va
|
||||
NOTE: MPEINIT: work.browser_info:
|
||||
url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB
|
||||
dc_version=7.15.0 adapter_version=4.19.0 browser=Chrome browser_version=120.0
|
||||
platform=Linux user_agent=...
|
||||
```
|
||||
|
||||
A service called directly (for instance from a URL, or by a script), or by an older frontend, has no `work.browser_info` - the log then records that fact instead.
|
||||
The page URL parameters, one row per parameter. Read a parameter by name rather than parsing the page URL - the platform's own parameters are in there too. Columns and examples: [Browser Variables](/developer-guide/browser-variables/#browser_url_vars).
|
||||
|
||||
### Reading the tables from a hook
|
||||
|
||||
A [hook script](macros.md) is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at:
|
||||
|
||||
```
|
||||
https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va
|
||||
```
|
||||
|
||||
`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - so a hook can pick up either by name:
|
||||
|
||||
```sas
|
||||
%let labels=N;
|
||||
%let embed=N;
|
||||
|
||||
%if %sysfunc(exist(work.browser_url_vars)) %then %do;
|
||||
proc sql noprint;
|
||||
select value into :labels
|
||||
from work.browser_url_vars
|
||||
where name = 'labels';
|
||||
select value into :embed
|
||||
from work.browser_url_vars
|
||||
where name = 'embed';
|
||||
quit;
|
||||
%end;
|
||||
```
|
||||
|
||||
To branch on what the client reports:
|
||||
|
||||
```sas
|
||||
%if %sysfunc(exist(work.browser_info)) %then %do;
|
||||
data _null_;
|
||||
set work.browser_info;
|
||||
call symputx('dc_timezone', timezone);
|
||||
call symputx('dc_locale', locale);
|
||||
run;
|
||||
%end;
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Guard with `%sysfunc(exist())` before reading. A service called directly, or by a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` fails the job.
|
||||
Guard the read with `%sysfunc(exist())` - a service called directly has no `work.browser_info`. Worked examples: [Browser Variables](/developer-guide/browser-variables/#reading-them-from-a-hook).
|
||||
@@ -1,7 +1,7 @@
|
||||
# Data Controller for SAS: Viewer
|
||||
|
||||
The viewer screen provides a raw view of the underlying table.
|
||||
Choose a library, then a table, and click view to see the first 500 rows.
|
||||
Choose a library, then a table, and click view to see the first rows - up to the `DC_MAXOBS_WEBVIEW` limit, 2,000 rows by default.
|
||||
A filter option is provided should you wish to view a different section of rows.
|
||||
|
||||
The following libraries will be visible:
|
||||
@@ -21,9 +21,9 @@ The search covers **every** column of the table at once, so you do not need to k
|
||||
- **Character search** (the default) is a case sensitive CONTAINS match against every character column, so `Smith` finds `Smithson` and `smith` finds `Goldsmith`, but `smith` will not find `Smithson`.
|
||||
- **Numeric search** (tick the Numeric box) is an exact match against every numeric column, so `42` will not find `420` or `4210`.
|
||||
- If a filter is applied, the search runs within the filtered rows rather than the whole table.
|
||||
- The grid is capped by the [`DC_MAXOBS_WEBVIEW`](/dcc-options/#dc_maxobs_webview) option (500 rows by default).
|
||||
- For a search, the cap applies to the row count as well: a search that matches more rows than the cap returns 500 of them, and the count shown next to the table name is the number of rows returned, not the total number of matches.
|
||||
- Without a search, the count shown next to the table name is the full number of rows in the filtered view, so it can be higher than the number of rows displayed - a table of 1,200 rows reports `1,200 rows` above a grid holding the first 500.
|
||||
- The grid is capped by the [`DC_MAXOBS_WEBVIEW`](/dcc-options/#dc_maxobs_webview) option (2,000 rows by default).
|
||||
- For a search, the cap applies to the row count as well: a search that matches more rows than the cap returns 2,000 of them, and the count shown next to the table name is the number of rows returned, not the total number of matches.
|
||||
- Without a search, the count shown next to the table name is the full number of rows in the filtered view, so it can be higher than the number of rows displayed - a table of 5,000 rows reports `5,000 rows` above a grid holding the first 2,000.
|
||||
- A search that matches nothing shows a "No data found with given conditions" panel rather than an empty grid.
|
||||
|
||||

|
||||
|
||||
@@ -175,4 +175,4 @@ It is possible to embed a Data Controller table within SAS Visual Analytics by s
|
||||
|
||||
To make the portlet more visually appealing, the Data Controller title bar can be removed by adding `?embed=true` to the URL. When opening in a new window, the title bar will be gone.
|
||||
|
||||
For a deeper VA integration - where report row selections drive filters and column visibility in the Data Controller editor - append `?embed=va` instead. See the [SAS Visual Analytics Embed](/embed-va/) page for details on filter modes, configuration, and debugging.
|
||||
For a deeper VA integration - where report row selections drive filters and column visibility in the Data Controller editor - append `?embed=va` instead. See the [SAS Visual Analytics Embed](/visual-analytics/) page for details on filter modes, configuration, and debugging.
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
layout: article
|
||||
title: Derived Variables
|
||||
description: Derived variables are computed columns that appear in the EDIT screen and can be filtered on, without being stored in the target table.
|
||||
---
|
||||
|
||||
# Derived Variables
|
||||
|
||||
A derived variable is a column that does not exist in the target table, but is computed when the table is loaded into the [EDIT screen](/dc-userguide/#editor). It is read-only, it appears after the physical columns, and it can be filtered on like any other column.
|
||||
|
||||
Derived variables are useful when users need to work with a value that is a function of the stored data - a banding, a concatenation, a calculation - without changing the table, the load process, or the ETL.
|
||||
|
||||
The values are **not stored**. They are computed on every read, and nothing derived is written back when a change is submitted or approved.
|
||||
|
||||
## What it looks like
|
||||
|
||||
The table below is shown in the EDIT screen. `LINE_VALUE` is `QTY * PRICE` and `REGION` is derived from `STATE`; neither exists in the table itself, and both are read-only.
|
||||
|
||||

|
||||
|
||||
The computed columns are marked apart from the physical ones: a tinted header with an `fx` badge, tinted italic cells, and a left edge where the derived block begins. They are read-only, and the marking says so - a column that is locked by a rule or by column level security is dimmed instead, which is a different message from "computed here". Hovering a header shows the variable's label, or its name and expression where no label was configured.
|
||||
|
||||
Because the derived columns are created before the filter is applied, they can be filtered like any other column. Here `REGION = 'West'` selects the two western states, and the filter bar names the derived variable:
|
||||
|
||||

|
||||
|
||||
A numeric derived column works the same way - `LINE_VALUE > 50` leaves the one row that satisfies it:
|
||||
|
||||

|
||||
|
||||
## How it works
|
||||
|
||||
The statements are written into a DATA step view over the target table, and the filter is applied to that view rather than to the table:
|
||||
|
||||
```sas
|
||||
data work.vw_derived / view=work.vw_derived;
|
||||
set &libds;
|
||||
attrib REGION length=$8 label='...' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
run;
|
||||
|
||||
data out;
|
||||
set work.vw_derived;
|
||||
where %inc filtref;;
|
||||
run;
|
||||
```
|
||||
|
||||
A `WHERE` statement applies to the data set named on the `SET` statement, so in a single step it cannot see a variable created later in the same step. Putting the derived variables on the view means they are on the input to the step that filters, which is what makes them filterable at all.
|
||||
|
||||
## Configuration
|
||||
|
||||
Derived variables are configured per table in the [MPE_DERIVED](/tables/mpe_derived/) control table, which is itself a Data Controller table - so it is maintained through the EDIT screen with the usual review and approve workflow. The three variables shown above, and nothing else, are the three rows below:
|
||||
|
||||

|
||||
|
||||
|
||||
Each row defines one variable:
|
||||
|
||||
- `LIBREF` / `DSN` - the target table
|
||||
- `VAR_NM` - the name of the new column
|
||||
- `VAR_DEPS` - every input the expression uses: the physical columns and any derived variables, space separated
|
||||
- `VAR_TYPE` - `C` for character, `N` for numeric
|
||||
- `VAR_LEN` - character length, or numeric length (1-8)
|
||||
- `VAR_FMT` - optional, eg `DATE9.` or `DOLLAR12.2`
|
||||
- `VAR_LABEL` - optional; when blank the label is `<VAR_NM>=<expression>`
|
||||
- `VAR_LOGIC` - the SAS expression that produces the value
|
||||
- `ACTIVE` - set to 1 to apply the variable
|
||||
|
||||
A simple example, deriving a region from a state:
|
||||
|
||||
```
|
||||
VAR_NM REGION
|
||||
VAR_DEPS STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 8
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL (blank)
|
||||
VAR_LOGIC ifc(STATE in ('CA','OR','WA'),'West','East')
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
A second variable can build on it - its dependencies include the physical column and the derived one:
|
||||
|
||||
```
|
||||
VAR_NM REGION_LABEL
|
||||
VAR_DEPS REGION STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 14
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL Region and state
|
||||
VAR_LOGIC cats(REGION,'-',STATE)
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
There is no sequence number to maintain. The statements are emitted in dependency order, so `REGION` is always assigned before `REGION_LABEL`, however the rows are ordered in the table.
|
||||
|
||||
For each row, Data Controller generates the following statements when the table is read:
|
||||
|
||||
```sas
|
||||
attrib REGION length=$8 label='REGION=ifc(STATE in (''CA'',''OR'',''WA''),''West'',''East'')' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
```
|
||||
|
||||
The label is `VAR_LABEL` when one is supplied, otherwise the variable name, an equals sign and the expression - `REGION=ifc(...)` - so the column shows both what it is and where it comes from when labels are switched on. Either way the label is truncated to the maximum label length, on a boundary that cannot split a doubled quote.
|
||||
|
||||
The type and length are declared, not inferred, so a value is truncated exactly as you specify. If `VAR_FMT` is left blank, character variables default to `$<length>.` and numeric variables to `<length>.`; a date, time or datetime format makes the column render (and filter) as a date, time or datetime, the same way a stored column does.
|
||||
|
||||
## Filtering
|
||||
|
||||
The derived variable is created before the filter is applied, so it can be used in the [filter](/filter/) like any other column - including its list of values. A filter on a derived column is stored and re-applied in the normal way, and the value list for a derived column is built from the table under the rest of the filter, exactly as for a stored column.
|
||||
|
||||
Derived columns themselves are an EDIT screen feature: the VIEW screen and the download services do not show them. A saved filter that references a derived column still applies there, and is evaluated the same way.
|
||||
|
||||
## Rules and limits
|
||||
|
||||
- **Expressions only.** `VAR_LOGIC` must be a single SAS expression - no semicolons, macro triggers (`%`, `&`) or comments. Conditional logic is available through functions such as `ifc()`, `choosec()`, `cats()`, `catx()`, `put()`, `input()` and `intnx()`.
|
||||
- **Dependencies declared.** `VAR_DEPS` lists every input the expression uses - physical columns and derived variables alike. Statements are generated in dependency order, so no sequence number is maintained, and the declaration is checked in both directions: every reference must be declared, and every declared name must be referenced and must exist. Circular references are rejected.
|
||||
- **No collisions.** `VAR_NM` must not match an existing column of the target table, or `_____DELETE__THIS__RECORD_____`.
|
||||
- **Read-only.** Derived columns cannot be edited in the grid, and any value submitted for them is discarded before the approval package is created.
|
||||
- **Label is free text.** `VAR_LABEL` takes any characters, in any script - quotes are escaped when the statement is generated - but it is a column label, so it is one line: control characters and line breaks are refused, and so is a value over 256 characters, which the engine would otherwise cut down silently.
|
||||
- **Not for row level security.** A derived variable cannot be named by an active [row level security](/row-level-security/) rule, of any scope. RLS rules are validated against the physical table, where a derived column does not exist, so a rule naming one is rejected when it is submitted.
|
||||
- **Recomputed on every read.** The value always reflects the current data and the current definition, and there is no history of previous derived values.
|
||||
|
||||
A configuration error - a bad name, an invalid type or length, a name collision, or a reference to a derived variable that is not declared in `VAR_DEPS` - stops the EDIT screen with a message naming the variable. A syntax error or an unknown column in the expression itself is caught when the view is compiled, and stops the screen with a message naming the table.
|
||||
|
||||
Most of these rules are also applied when a row is submitted for approval, through a post edit hook on `MPE_DERIVED`. A bad name, type, length, format or expression - or a reference to a derived variable that is not declared in `VAR_DEPS` - is rejected at submit time, while the error can still be corrected, rather than breaking the EDIT screen afterwards. The collision between `VAR_NM` and a physical column of the target table is only checked at read time, as the hook does not have the target library assigned.
|
||||
|
||||
The simple rules are also configured as validation rules on the table itself, so they show up in the grid before submission: `VAR_TYPE` is a dropdown of `C` and `N`, `VAR_LEN` carries minimum and maximum values, and `VAR_NM`, `VAR_DEPS`, `VAR_FMT` and `VAR_LOGIC` are pattern checked. Validation rules are evaluated in the browser, so the post edit hook remains the authoritative check.
|
||||
|
||||
!!! warning "Derived values can expose hidden data"
|
||||
An expression can reference a column that is hidden from the user by [Column Level Security](/column-level-security/), and the derived column will show the result. Configure derived variables with the same care as any other data exposure.
|
||||
|
||||
## Performance
|
||||
|
||||
The expression is evaluated for each row read, before the filter. The filter already requires a scan of the table, so this adds per-row arithmetic rather than a second pass - but keep expressions cheap (no lookups, no subqueries), and expect the cost to be paid on every load of the EDIT screen, including each request for filter values.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
layout: article
|
||||
title: Browser Variables
|
||||
description: The frontend sends the browser context to the backend as two input tables - browser_info and browser_url_vars - available to the startup service, the edit services and hook scripts.
|
||||
---
|
||||
|
||||
# Browser Variables
|
||||
|
||||
Two input tables travel with most requests, so a SAS program can see the browser it is serving rather than asking the user:
|
||||
|
||||
| Table | Holds |
|
||||
|---|---|
|
||||
| `work.browser_info` | one row - the page URL, the document embedding it, the browser timezone and locale, and the versions in play |
|
||||
| `work.browser_url_vars` | one row per parameter on the page URL |
|
||||
|
||||
They exist for two jobs: diagnosing a support ticket from the job log, and letting a [hook script](/dcc-tables/#hook-scripts) adapt to how the app was opened - embedded in a VA report, opened with `?labels=true`, or used by someone in another timezone.
|
||||
|
||||
## Which services receive them
|
||||
|
||||
`public/startupservice`, `editors/getdata`, `editors/getdynamiccolvals`, `editors/stagedata`, `editors/loadfile`, `editors/restore` and `auditors/postdata`. Other services do not receive them.
|
||||
|
||||
`editors/loadfile` is the exception in practice: the app reaches it through the adapter's multipart upload, which carries no input tables, so the service - and the post edit hook it runs - does not receive them in normal use. It is on the list for completeness, and would receive them if it were ever invoked through the ordinary request path.
|
||||
|
||||
## browser_info
|
||||
|
||||
A single row with these columns:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it. When the editor is embedded in a report, the embedding URL is visible here. |
|
||||
| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL, so the embedding report can be told apart from the editor URL. |
|
||||
| `timezone` | The browser's IANA timezone name, e.g. `Europe/Berlin`. |
|
||||
| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it (positive west of UTC, so `Europe/Berlin` in summer is -120). |
|
||||
| `locale` | The browser locale, e.g. `en-GB`. |
|
||||
| `dc_version` | The Data Controller build, as shown by `appinfo()`. |
|
||||
| `adapter_version` | The `@sasjs/adapter` version the client was built with. |
|
||||
| `browser` | The browser family, parsed from the user agent. |
|
||||
| `browser_version` | The browser version. |
|
||||
| `platform` | The operating system, parsed from the user agent. |
|
||||
| `user_agent` | The raw `navigator.userAgent` string. |
|
||||
|
||||
## browser_url_vars
|
||||
|
||||
The page URL parameters, one row per parameter, so a SAS program can read a parameter by name rather than parse the `url` string:
|
||||
|
||||
| Column | Description |
|
||||
|---|---|
|
||||
| `name` | The URL parameter name. |
|
||||
| `value` | The URL parameter value. |
|
||||
|
||||
Parameters from both the search string and the hash query string (Angular routes carry them after the `#`) are included; on a name collision the search string value wins. The table is sent only when the page URL has at least one parameter.
|
||||
|
||||
On SAS 9 and Viya the app is reached through the platform's own job URL, so the platform's parameters (`_FILE`, `_program`, `_debug`, `_webout` and friends) are in the table alongside Data Controller's. Read a parameter by name rather than assuming every row belongs to the app - `embed` and `labels` are Data Controller's, `_FILE` and `_program` are the platform's.
|
||||
|
||||
## Seeing them in the job log
|
||||
|
||||
Run any of the services above with debug on. The value depends on the path: `&_debug=131` on the Compute API and SAS 9, or `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled - which is what the frontend sends there, so turning debug on in the app is enough. Either value makes the session initialisation write both tables to the job log:
|
||||
|
||||
```
|
||||
NOTE: MPEINIT: work.browser_url_vars:
|
||||
name=embed value=va
|
||||
NOTE: MPEINIT: work.browser_info:
|
||||
url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB
|
||||
dc_version=7.15.0 adapter_version=4.19.0 browser=Chrome browser_version=120.0
|
||||
platform=Linux user_agent=...
|
||||
```
|
||||
|
||||
A service called directly (for instance from a URL, or by a script), or by a frontend older than this feature, has no `work.browser_info` - the log then records that fact instead.
|
||||
|
||||
## Reading them from a hook
|
||||
|
||||
A [hook script](/dcc-tables/#hook-scripts) is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at:
|
||||
|
||||
```
|
||||
https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va
|
||||
```
|
||||
|
||||
`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - so a hook can pick up either by name:
|
||||
|
||||
```sas
|
||||
%let labels=N;
|
||||
%let embed=N;
|
||||
|
||||
%if %sysfunc(exist(work.browser_url_vars)) %then %do;
|
||||
proc sql noprint;
|
||||
select value into :labels
|
||||
from work.browser_url_vars
|
||||
where name = 'labels';
|
||||
select value into :embed
|
||||
from work.browser_url_vars
|
||||
where name = 'embed';
|
||||
quit;
|
||||
%end;
|
||||
```
|
||||
|
||||
To branch on what the client reports:
|
||||
|
||||
```sas
|
||||
%if %sysfunc(exist(work.browser_info)) %then %do;
|
||||
data _null_;
|
||||
set work.browser_info;
|
||||
call symputx('dc_timezone', timezone);
|
||||
call symputx('dc_locale', locale);
|
||||
run;
|
||||
%end;
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Guard with `%sysfunc(exist())` before reading. A service called directly, or a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` fails the job.
|
||||
|
||||
## What they are not
|
||||
|
||||
Both tables are supplied by the client, so treat them as diagnostic hints rather than a security boundary. Never authorise on anything in them - a caller can send whatever it likes.
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
layout: article
|
||||
title: Updating the Filter in a Pre-Edit Hook
|
||||
description: Read the filter a user applied, and hand back a modified one, from a Data Controller pre-edit hook - including changing the rows the editor shows.
|
||||
---
|
||||
|
||||
# Updating the Filter in a Pre-Edit Hook
|
||||
|
||||
A [pre-edit hook](/dcc-tables/#pre_edit_hook) can read the filter a user applied and return a modified one. This is useful when the editor should widen a user's selection behind the scenes - for example, when a user filters for the current quarter and the grid should also show the previous one.
|
||||
|
||||
The hook runs inside the `editors/getdata` service, so both the applied filter and the filtered rows are in scope. This page describes what is available at that point and works through an example.
|
||||
|
||||
## What the editor receives
|
||||
|
||||
When a filtered editor loads, `editors/getdata` returns the filter to the client in two forms:
|
||||
|
||||
- `sasparams.FILTER_TEXT` - the resolved WHERE clause, which the editor shows above the grid.
|
||||
- `query` - the filter's clauses as structured rows (`variable_nm`, `operator_nm`, `raw_value`), which the editor renders as the filter chips.
|
||||
|
||||
The response does not carry the filter key (`FILTER_RK`). That is an *input* to `getdata`, not an output - the editor already holds it in the URL (`/editor/<table>/<FILTER_RK>`), so there is nothing to return.
|
||||
|
||||
The hook also receives the browser tables that came in with the request - `work.browser_info` and `work.browser_url_vars` - so it can tell an embedded editor from a standalone one, or take a value from the page URL. See [Browser Variables](/developer-guide/browser-variables/).
|
||||
|
||||
## When the hook runs
|
||||
|
||||
`getdata` builds the filter, applies it, and sorts the result, all before the pre-edit hook runs. In order:
|
||||
|
||||
1. the WHERE clause is written to a temporary fileref, `filtref`;
|
||||
2. `work.OUT` is populated with `where %inc filtref` - the filter is applied here;
|
||||
3. the pre-edit hook runs;
|
||||
4. `FILTER_TEXT` is read back out of `filtref`, and the response is streamed.
|
||||
|
||||
So the hook sits between "the rows are filtered" and "the filter is returned". Two consequences follow:
|
||||
|
||||
- It can change what the editor is told the filter was, by rewriting `filtref` and, if the chips must match, `work.query`.
|
||||
- It cannot change which rows came back *that* way, because the data step has already run. To return different rows, the hook must re-read the table itself.
|
||||
|
||||
## Take the value from the data
|
||||
|
||||
A hook that wants to extend a date filter needs the date the user is working with. It does not have to parse the filter text for it - the filtered rows are already in `work.OUT`, so the value can be read from there.
|
||||
|
||||
## Example: widen a date filter to the previous quarter
|
||||
|
||||
The hook below is registered in `MPE_TABLES.PRE_EDIT_HOOK` for a table with a date column `ORDER_DATE`. When a user opens the editor with a filter that includes `ORDER_DATE`, it extends the filter to also cover the previous quarter, re-reads the table so the grid shows both quarters, and returns the widened filter text.
|
||||
|
||||
```sas
|
||||
/* PRE_EDIT_HOOK: widen the user's ORDER_DATE filter by one quarter */
|
||||
|
||||
%if &filter_rk > 0 %then %do;
|
||||
|
||||
/* the date the user is looking at, taken from the rows already returned */
|
||||
%let refdt=;
|
||||
proc sql noprint;
|
||||
select max(ORDER_DATE) into :refdt from work.out;
|
||||
quit;
|
||||
|
||||
%if &refdt ne %then %do;
|
||||
|
||||
/* the previous quarter, relative to that date */
|
||||
%let pq_from=%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,b)),date9.));
|
||||
%let pq_to =%sysfunc(putn(%sysfunc(intnx(quarter,&refdt,-1,e)),date9.));
|
||||
|
||||
/* the user's own clauses, as a standalone WHERE */
|
||||
%mp_filtergenerate(work.query,outref=userref)
|
||||
|
||||
/* Data Controller's own clause - current-record validity, plus RLS */
|
||||
%mpe_filtermaster(EDIT,&orig_libds,
|
||||
dclib=&mpelib,
|
||||
filter_rk=-1,
|
||||
outref=ctlref,
|
||||
outds=work.ctlq
|
||||
)
|
||||
|
||||
/* read each clause (which may span several lines) into a macro variable */
|
||||
%let usertext=;
|
||||
data _null_;
|
||||
infile userref end=eof;
|
||||
input;
|
||||
length t $32767;
|
||||
retain t;
|
||||
t=catx(' ',t,_infile_);
|
||||
if eof then call symputx('usertext',t);
|
||||
run;
|
||||
%let ctltext=;
|
||||
data _null_;
|
||||
infile ctlref end=eof;
|
||||
input;
|
||||
length t $32767;
|
||||
retain t;
|
||||
t=catx(' ',t,_infile_);
|
||||
if eof then call symputx('ctltext',t);
|
||||
run;
|
||||
|
||||
/* ( user's filter OR previous quarter ) AND ( validity + RLS ) */
|
||||
filename newfilt temp;
|
||||
data _null_;
|
||||
file newfilt lrecl=32767;
|
||||
length f $32767;
|
||||
f='(( '!!trim(symget('usertext'))!!' ) OR (ORDER_DATE between "&pq_from"d and "&pq_to"d))'
|
||||
!!' AND ('!!trim(symget('ctltext'))!!' )';
|
||||
put f;
|
||||
run;
|
||||
|
||||
/* re-read the table, so the grid shows both quarters */
|
||||
data work.out (drop=&var_txfrom &var_txto &var_processed &drop_rk);
|
||||
set &orig_libds;
|
||||
where %inc newfilt;
|
||||
run;
|
||||
|
||||
/* keep the order the editor first showed */
|
||||
proc sort data=work.out;
|
||||
by &sortpk;
|
||||
run;
|
||||
|
||||
/* hand the widened filter back to the editor */
|
||||
data _null_;
|
||||
file filtref lrecl=32767;
|
||||
infile newfilt;
|
||||
input;
|
||||
put _infile_;
|
||||
run;
|
||||
|
||||
%end;
|
||||
%end;
|
||||
```
|
||||
|
||||
### What the hook does
|
||||
|
||||
- **The reference date.** `work.OUT` holds the rows the user's filter returned, so the latest `ORDER_DATE` on screen is read straight from it. Nothing has to be parsed out of the filter text.
|
||||
- **The previous quarter.** `INTNX` moves back one quarter from that date and returns its first and last day, formatted as SAS date literals (`01APR2026` and `30JUN2026`, for example).
|
||||
- **The user's clause.** `work.QUERY` is the filter's clauses as structured rows, and `%mp_filtergenerate` turns them back into a WHERE clause. This is the user's filter on its own, with no Row Level Security or validity clause mixed in.
|
||||
- **Data Controller's clause.** Calling `%mpe_filtermaster` with `filter_rk=-1` produces the part of the filter that is not the user's: the current-record validity clause and any Row Level Security, or `1=1` when there is neither.
|
||||
- **The combined filter.** The two clauses are combined as `( user OR previous-quarter ) AND ( validity + RLS )`, so the previous quarter is added to the user's selection without widening anything the user is not entitled to see.
|
||||
- **The rows.** `work.OUT` is rebuilt with the combined filter, using the same drop list as the data step `getdata` runs itself - the transaction and processing columns, plus `&drop_rk`, the surrogate key that `getdata` hides when a table is registered with `RK_UNDERLYING` - and sorted on `&sortpk`, because `getdata` sorts before the hook runs and does not sort again afterwards. On a bitemporal table `getdata` also deduplicates to the latest record per primary key (`by &pk &var_busfrom`); add that step too, or the re-read returns every temporal version rather than the current one. The delete column is **not** re-added: `_____DELETE__THIS__RECORD_____` is created by the editor itself and is never a column of the dataset, so a hook must not introduce it (a response that carries that name is refused by the editor).
|
||||
- **The returned text.** `filtref` is overwritten with the combined filter, so the clause the editor shows is the one that was actually applied.
|
||||
|
||||
### What it looks like
|
||||
|
||||
The captures below are the editor for a table with an `ORDER_DATE` column, opened with a filter for the current quarter. In the first, no hook is configured, so the filter is the user's own and the grid holds one quarter of rows. In the second, the hook has run: the filter above the grid carries both quarters, and the rows have grown to match.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Row Level Security
|
||||
|
||||
The user's clause and Data Controller's clause have to be kept apart, because Row Level Security lives in the second one. Combining them as `( user OR previous-quarter ) AND ( validity + RLS )` keeps RLS applied to both branches. Wrapping the whole existing filter instead - `( existing OR previous-quarter )` - would let the previous-quarter rows through without their RLS condition, so do not take that shortcut.
|
||||
|
||||
## The row and cell caps
|
||||
|
||||
`getdata` truncates a result over either `DC_MAXOBS_WEBEDIT` or `DC_MAXCELLS_WEBEDIT`, whichever binds first - a wide table runs out of cells before rows, at floor(cells / columns) rows - and warns against the table name. Widening a filter widens the result, so a hook that adds a quarter can push a table over the cap: the rows past the limit never reach the browser. Check both counts before handing the data back, or raise the options for that table in `MPE_CONFIG`.
|
||||
|
||||
## Matching the filter chips
|
||||
|
||||
The clause above the grid comes from `FILTER_TEXT`, so overwriting `filtref` is what updates it. The filter chips are rendered from the `query` output instead, so if they must reflect the widened filter too, amend `work.QUERY` in the same hook - append the extra clause as a new subgroup with `group_logic='OR'` before the response is built.
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
layout: article
|
||||
title: One Table, Two Rule Sets
|
||||
description: A single physical table can carry only one set of Data Controller validation rules. Two librefs over the same data - or a pair of hook scripts - give you as many rule sets as you need.
|
||||
---
|
||||
|
||||
# One Table, Two Rule Sets
|
||||
|
||||
Most Data Controller sites settle into an obvious mapping: one table, one edit screen, one set of validation rules. But that is not always what the business wants. A table of orders might be edited from a finance report that only tolerates small adjustments, and from an operations report where much larger ones are routine. Same table, same columns, same approvers - different rules.
|
||||
|
||||
Data Controller's validation rules are configured per table, so this takes a little thought. There are two ways to do it: the one we recommend, and the one to reach for when the first is not available.
|
||||
|
||||
## Why one table is one rule set
|
||||
|
||||
Two configuration tables decide this.
|
||||
|
||||
`MPE_TABLES` is the list of editable tables, and its primary key is `(tx_from, libref, dsn)`. One physical table is one editable table.
|
||||
|
||||
`MPE_VALIDATIONS` holds the rules, and its primary key is `(tx_from, base_lib, base_ds, base_col, rule_type)`. Rules hang off a physical `libref.dataset`. There is no per-menu or per-report scoping anywhere in the schema, and the editor is handed exactly the rules whose `base_lib` and `base_ds` match the table being opened.
|
||||
|
||||
So two rule sets on one table need two distinct `libref.dataset` identities. The question is how to get them without copying the data.
|
||||
|
||||
## Option 1 (recommended): two librefs over the same data
|
||||
|
||||
A libref is just a name pointing at a location. Nothing stops you assigning two of them to the same place, and Data Controller will treat the two as separate tables:
|
||||
|
||||
```sas
|
||||
libname ORDERS_EU '/data/orders';
|
||||
libname ORDERS_US '/data/orders';
|
||||
```
|
||||
|
||||
`ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` are now the same physical file, but they are different rows in `MPE_TABLES` and can therefore carry different rows in `MPE_VALIDATIONS`. Register both, give each its own rules, and point each report at its own editor URL - `#/editor/ORDERS_EU.ORDERS` and `#/editor/ORDERS_US.ORDERS`.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Everything else works exactly as it always did. Filtering, search, the row cap, the approval diff and the audit trail all operate on the table as normal, because as far as Data Controller is concerned these are ordinary tables. The only difference is that the two names resolve to the same file, so an approval in either report updates the same data - and that one difference has a consequence for concurrency, which is covered below.
|
||||
|
||||
There is no copy to keep in sync, no hook to write and nothing to maintain. That is why it is the option we recommend.
|
||||
|
||||
### Things worth knowing
|
||||
|
||||
- Where the librefs are defined depends on your platform. On Viya, in the compute context's `autoexec.sas` (or `[DC Drive Path]/services/settings.sas`); on SAS 9, as metadata libraries or in the Data Controller Settings stored process; on SASjs Server, in `services/public/settings.sas`. The one requirement is that each library has a unique libref.
|
||||
- `mp_lockanytable` keys on `libref.dataset`, so the two menus do not serialise against each other. That has consequences beyond a collision - see *The concurrency caveat* below.
|
||||
- The audit trail and approval queue record which libref a change came through, so `ORDERS_EU.ORDERS` and `ORDERS_US.ORDERS` stay distinguishable in history. For most people that is a feature - you can see which report a change originated from.
|
||||
|
||||
### The concurrency caveat
|
||||
|
||||
Data Controller serialises its writes with `mp_lockanytable`, and the control table it uses, `MPE_LOCKANYTABLE`, has a primary key of `(lock_lib, lock_ds)`. That is the *registration*, not the physical file. At approve time, `postdata` takes the lock on the base table named in the submit record, and the same service runs its "has this table been updated since the diff screen was shown" check against `MPE_DATALOADS`, keyed on that same `libref` and `dsn`.
|
||||
|
||||
Two librefs over one location are two different `(libref, dsn)` pairs, so:
|
||||
|
||||
- two approvals arriving through different reports take two different locks, and neither one blocks the other;
|
||||
- a load through one report writes its `MPE_DATALOADS` entry against its own name, so the other report's staleness check does not see it either.
|
||||
|
||||
So two people editing the same rows through different reports can both be approved, and the second write wins, silently. The lock is advisory to begin with - `mp_lockanytable` is, in its own words, "only useful if every update uses the macro" - so this is not a new class of risk, but registering one file twice is a new way to fall into it.
|
||||
|
||||
The hook route does not have this problem. Both mirrors route their submit to the same base table, so every approval locks, loads and logs against one identity.
|
||||
|
||||
If you do use two librefs, and concurrency matters to you, the cleanest fix is to keep the two registrations for the edit screen and give each one a `POST_EDIT_HOOK` that re-points the changeset at a single canonical registration - then every approval is keyed on the same table regardless of which report raised it. Register that canonical name as well. The alternative, an explicit shared lock taken in `PRE_APPROVE_HOOK` and released in `POST_APPROVE_HOOK`, works too, but a failed run leaves the sentinel locked and `MPE_LOCKANYTABLE` is then a table you have to unpick by hand.
|
||||
|
||||
## Option 2: an empty mirror and a pair of hook scripts
|
||||
|
||||
Sometimes two librefs over one location are not available: a database library where the platform will not let you define the same object twice, or a site where adding a library definition is a change nobody wants to make. Then you can reach the same result with a mirror table and two hook scripts.
|
||||
|
||||
The idea is that the thing Data Controller edits is not the real table at all, but an empty table of the same shape, with hooks moving data in and out of it:
|
||||
|
||||
- a `PRE_EDIT_HOOK` fills the editor with the live rows of the real table, so the mirror never has to hold a copy
|
||||
- a `POST_EDIT_HOOK` re-points the submitted changeset at the real table, so the approval is raised against the real table and the load writes there
|
||||
|
||||
The mirror exists purely to carry the rule set, and never stores any data.
|
||||
|
||||
### The pre-edit hook
|
||||
|
||||
`PRE_EDIT_HOOK` runs inside the `getdata` service, after the user's filter has been applied and the rows sorted, with the data in `work.OUT`. It may replace that dataset, which is all this needs:
|
||||
|
||||
```sas
|
||||
data work.out;
|
||||
set ORDERS.ORDERS;
|
||||
run;
|
||||
```
|
||||
|
||||
The registered table is `ORDERS.MIRROR`, which is empty - so without the hook the editor would show nothing at all. With it, the grid shows the live rows:
|
||||
|
||||

|
||||
|
||||
Note the title bar: the mirror really is empty. Everything on screen came from the hook, and the grid is validated against the mirror's own rules rather than the real table's.
|
||||
|
||||
### The post-edit hook
|
||||
|
||||
This is the part that surprises people. `POST_EDIT_HOOK` runs inside the `mpe_loader` macro at submit time, on the staged rows, before the submit record is written. It cannot choose the target table directly - but at that point `LIBREF` and `DS` are still ordinary macro variables, and the submit record is built from them. Reassigning them re-points the whole changeset:
|
||||
|
||||
```sas
|
||||
data _null_;
|
||||
call symputx('libref','ORDERS');
|
||||
call symputx('ds','ORDERS');
|
||||
run;
|
||||
```
|
||||
|
||||
From that moment the changeset is an approval against `ORDERS.ORDERS`. The approver sees a diff against the real table, the load writes to the real table, and the mirror is never touched.
|
||||
|
||||
While it runs, the hook can also read `work.browser_info` and `work.browser_url_vars` - the browser context that came with the submit - to tell an embedded editor from a standalone one, or to pick up a parameter from the page URL. See [Browser Variables](/developer-guide/browser-variables/).
|
||||
|
||||

|
||||
|
||||
### Reassigning LIBREF and DS
|
||||
|
||||
`LIBREF` and `DS` are ordinary macro variables inside `mpe_loader`, which creates them with `%let` and then updates them from the staged row. A `%let libref=ORDERS;` reassigns them just as well as the `call symputx` above: both statements assign to the *existing* variable, because it lives in an enclosing symbol table (`mpe_loader`'s local table) and SAS only creates a new local when the name is absent from every enclosing table. `call symputx` is used above because it is explicit about which variable is being set, and because it does not depend on the enclosing scope still holding the name - a wrapper macro that declared `%local libref` would turn a bare `%let` into an assignment to that shadow instead.
|
||||
|
||||
### Other things to watch
|
||||
|
||||
- Concurrency behaves correctly here. Both mirrors route their submit to the same base table, so every approval locks, loads and logs against one identity - unlike the two-libref route above.
|
||||
- The filter has already been applied to the empty mirror by the time the pre-edit hook runs, so a hook that reads the real table ignores the user's filter unless it re-applies it (`where %inc filtref`). On a small table you will not notice; on a large one the `DC_MAXOBS_WEBEDIT` or `DC_MAXCELLS_WEBEDIT` cap truncates the edit screen, with a warning against the table name. [Updating the Filter in a Pre-Edit Hook](/developer-guide/filter-pre-edit-hook/) works through extending that filter.
|
||||
- The hook's output must have the same columns the editor expects - the real table, minus any transaction or processing columns that Data Controller drops on load.
|
||||
- The real table must itself be registered in `MPE_TABLES`. The approval screen resolves the table's audit settings from that row, so a changeset routed to a table with no registration cannot be reviewed - the submit is refused up front, naming the table.
|
||||
- The mirror's `MPE_TABLES` row is read for the edit screen and the real table's for the load, so keep their `buskey`, `loadtype` and temporal column settings identical.
|
||||
- At approval time the access checks run against the real table, so editors need `EDIT` on the mirror while approvers need `EDIT` and `APPROVE` on the real table.
|
||||
|
||||
## Which should you use?
|
||||
|
||||
If you can define two librefs over the same data, do that. It is configuration only, it leaves every other behaviour of the editor untouched, and there is nothing to maintain.
|
||||
|
||||
Reach for the hook scripts when the platform will not let you duplicate the library definition, or when you specifically want the rule set to be a property of the application rather than of the data.
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
layout: article
|
||||
title: SAS Visual Analytics Embed
|
||||
description: Embed the Data Controller editor inside a SAS VA report as a data-driven content object. Row selections in the VA report drive filters and column visibility in the editor.
|
||||
---
|
||||
|
||||
# Embedding inside SAS Visual Analytics
|
||||
|
||||
Data Controller can be embedded inside a SAS Visual Analytics report as a **data-driven content** (DDC) object. This unlocks scenarios where a user can safely modify the values in an underlying SAS table, and have the visualisation updated- all from inside the same report.
|
||||
|
||||
## URL
|
||||
|
||||
Open the editor as a DDC by appending `?embed=va` to the editor route:
|
||||
|
||||
```
|
||||
|
||||
`https://yourserver/DC/#/editor/<library>/<table>?embed=va`
|
||||
|
||||
```
|
||||
|
||||
The `?embed=` parameter accepts three values:
|
||||
|
||||
| Value | Behaviour |
|
||||
|---|---|
|
||||
| `true` | Chrome (header, back button, sub-nav) is hidden. Editor functions normally. |
|
||||
| `va` | Same chrome-hiding as `true`, **plus** editor becomes "VA-aware" - any report filters are captured, and columns can be displayed / hidden using the Edit Report interface. Filter button is disabled (use VA filters instead)|
|
||||
| `false` (or omitted) | Standard interactive UI. |
|
||||
|
||||
## How VA drives the editor
|
||||
|
||||
VA pushes a JSON payload to the iframe via `window.postMessage` whenever the selected rows change. Data Controller listens for these messages and:
|
||||
|
||||
1. Resolves each VA `parameter` to a DC column by matching on column label.
|
||||
2. Builds a filter (using existing [Filter](/filter/) machinery) over the selected values.
|
||||
3. Hides any VA columns marked as `brush` so the grid stays focused on the user-editable columns (except primary key cols which are always shown)
|
||||
|
||||
If VA sends an empty / unmatched message the editor falls back to the unfiltered view but stays in VA mode.
|
||||
|
||||
More logic available in [`va-messaging.service.ts'](https://git.datacontroller.io/dc/dc/src/branch/main/client/src/app/services/va-messaging.service.ts).
|
||||
|
||||
## Filter Modes
|
||||
|
||||
When running in `embed=va` mode, the editor provides two filter modes, controlled by the **Auto-apply** checkbox:
|
||||
|
||||
* **Live (default)** - the editor updates automatically as you select rows in the VA report, so the data you see always matches your current selection.
|
||||
* **Confirm** - filter changes are held until you click the **Apply** button. This is useful when editing, where an automatic reload would discard unsaved changes.
|
||||
|
||||
A status indicator shows whether a filter change is pending, loading, or idle. In edit mode, a pending filter is held until you leave edit mode, so your unsaved edits are never lost.
|
||||
|
||||
## Configuration in VA
|
||||
|
||||
In the VA Report Designer, add a **Data-Driven Content** object and set the URL to the editor route shown above. Be sure that any report level filters have their corresponding parameters added to the DDC object itself.
|
||||
|
||||
## Debugging
|
||||
|
||||
The following snippet can be used in console to dump the values being provided to DC from VA:
|
||||
|
||||
```
|
||||
console.log(JSON.stringify(window.__vaLastMessage?.data, null, 2))
|
||||
```
|
||||
@@ -7,7 +7,9 @@ og_image: https://docs.datacontroller.io/img/filter_dynamic_on.png
|
||||
|
||||
# Filtering
|
||||
|
||||
Data Controller for SAS® enables you to create complex table filters. The "dynamic" setting enables the dropdown values to be pre-filtered by previous filter clauses. Filtered views are shareable! They are also used when [embedded in VA.](/embed-va)
|
||||
Data Controller for SAS® enables you to create complex table filters. The "dynamic" setting enables the dropdown values to be pre-filtered by previous filter clauses. Filtered views are shareable! They are also used when [embedded in VA.](/visual-analytics/)
|
||||
|
||||
The clause shown above the grid is the resolved WHERE clause that was applied, so it uses SAS syntax - a date, datetime or time filter reads as a SAS literal, eg `SOME_DATE = '20APR2042'd`, rather than the internal day or second count.
|
||||
|
||||
## Shared Filters
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 157 KiB |
@@ -42,7 +42,7 @@ Data Controller is regularly updated with new features. If you see something th
|
||||
* [Row Level Security](/row-level-security)
|
||||
* Excel [formula support](excel)
|
||||
* Dynamic [cell dropdown](/dynamic-cell-dropdown)
|
||||
* [SAS Visual Analytics embed](/embed-va/) - drive the editor from VA report selections
|
||||
* [SAS Visual Analytics embed](/visual-analytics/) - drive the editor from VA report selections
|
||||
* Works on ALL flavours of SAS (Base, EBI, Viya)
|
||||
|
||||
|
||||
|
||||
@@ -177,4 +177,8 @@ Delivered in v7.13.0. Formula columns (HARDFORMULA / SOFTFORMULA) are computed
|
||||
|
||||
### Regex Rules
|
||||
|
||||
Delivered. HARDREGEX (blocking) and SOFTREGEX (warning) rules validate cell values against a Perl-style regular expression. See [Regex Rules](/dcc-validations/#regex-rules).
|
||||
Delivered. HARDREGEX (blocking) and SOFTREGEX (warning) rules validate cell values against a Perl-style regular expression. See [Regex Rules](/dcc-validations/#regex-rules).
|
||||
|
||||
### Derived Variables
|
||||
|
||||
Computed columns can be configured per table, so the EDIT screen shows values that are not stored anywhere - a banding from a code, a multiplication of two columns - and they can be filtered on like any other column. The configuration lives in [MPE_DERIVED](/tables/mpe_derived/), and the grid marks the computed columns with an `fx` badge. See [docs](/derived-variables)
|
||||
@@ -41,6 +41,8 @@ This variable is an integer, and used to divide a complex filter into numerous s
|
||||
### RLS_VARIABLE_NM
|
||||
This is the name of the variable against which a filter value will be applied
|
||||
|
||||
It must be a physical column of the table. A [derived variable](/derived-variables/) cannot be named here: rules are validated against the table itself, where a derived column does not exist, so such a rule is rejected when it is submitted.
|
||||
|
||||
### RLS_OPERATOR
|
||||
The available operator will depend on whether the column is character or numeric. Example values:
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
layout: article
|
||||
title: MPE_DERIVED
|
||||
description: The MPE_DERIVED table is used to configure computed columns that appear in the EDIT screen and can be filtered on without being stored in the table.
|
||||
---
|
||||
|
||||
# MPE_DERIVED
|
||||
|
||||
The MPE_DERIVED table is used to configure derived (computed) variables - columns that are created when a table is loaded into the EDIT screen, so that they can be displayed and filtered on without being stored. More details are available in the [user guide](/derived-variables/)
|
||||
|
||||
The table is itself registered in [MPE_TABLES](/tables/mpe_tables/), so it is maintained through the EDIT screen with the usual review and approve workflow.
|
||||
|
||||
## Columns
|
||||
|
||||
- `TX_FROM num`: SCD2 open datetime
|
||||
- 🔑 `TX_TO num`: SCD2 close datetime
|
||||
- 🔑 `LIBREF char(8)`: The libref of the target table
|
||||
- 🔑 `DSN char(32)`: The target table
|
||||
- 🔑 `VAR_NM char(32)`: The name of the derived column
|
||||
- `VAR_DEPS char(4000)`: Every input the expression uses - physical columns and derived variables, space separated
|
||||
- `VAR_TYPE char(1)`: `C` for character, `N` for numeric
|
||||
- `VAR_LEN num`: Character length, or numeric length (1-8)
|
||||
- `VAR_FMT char(32)`: Optional SAS format, eg `DATE9.`
|
||||
- `VAR_LABEL char(256)`: Optional label; defaults to `<VAR_NM>=<expression>`
|
||||
- `VAR_LOGIC char(4000)`: The SAS expression that produces the value
|
||||
- `ACTIVE num`: Set to 1 to apply the variable
|
||||
@@ -17,5 +17,5 @@ The `MPE_FILTERSOURCE` table stores the individual query lines of each filter cr
|
||||
- `SUBGROUP_ID num`: Identifier of the subgroup to which this line belongs
|
||||
- `VARIABLE_NM char(32)`: The variable being filtered
|
||||
- `OPERATOR_NM char(12)`: The filter operator (e.g. `=`, `>`, `IN`)
|
||||
- `RAW_VALUE char(4000)`: The filter value
|
||||
- `RAW_VALUE char(4000)`: The filter value. A date, datetime or time is stored as a SAS literal (for example `'20APR2042'd`, `'01JAN1960:00:00:42'dt`, `'00:00:42't`), so the clause shown above the grid reads as a date rather than the internal day or second count.
|
||||
- `PROCESSED_DTTM num`: Datetime the filter line was created
|
||||
@@ -17,7 +17,7 @@ A collection of videos made in relation to Data Controller. Some of them were r
|
||||
### Data Controller - a Customer Story
|
||||
An illustration of the utility of Data Controller for Data & Analytics teams in regulated industries.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/M8hafkS4zY4" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<iframe title="Data Controller - a Customer Story" width="560" height="315" src="https://vid.4gl.io/videos/embed/kpSGoNE26FSif8X9A6pUfe?peertubeLink=0" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>
|
||||
|
||||
### Use Case Video - Manual Overrides
|
||||
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
layout: article
|
||||
title: SAS Visual Analytics Embed
|
||||
description: Embed the Data Controller editor inside a SAS Visual Analytics report as a data-driven content object, so report selections drive filters and column visibility in the editor.
|
||||
og_title: Embedding Data Controller in SAS Visual Analytics
|
||||
og_image: https://docs.datacontroller.io/img/va-embed-og.png
|
||||
---
|
||||
|
||||
# Embedding inside SAS Visual Analytics
|
||||
|
||||
Data Controller can be embedded inside a SAS Visual Analytics report as a **data-driven content** (DDC) object. This unlocks scenarios where a user can safely modify the values in an underlying SAS table, and have the visualisation updated - all from inside the same report.
|
||||
|
||||
## URL
|
||||
|
||||
Open the editor as a DDC by appending `?embed=va` to the editor route:
|
||||
|
||||
```text
|
||||
https://yourserver/DC/#/editor/<library>/<table>?embed=va
|
||||
```
|
||||
|
||||
The `?embed=` parameter accepts three values:
|
||||
|
||||
| Value | Data Controller header and navigation | Filter button | Report drives filtering and columns |
|
||||
|---|---|---|---|
|
||||
| `true` | hidden | **shown** - Data Controller's own filter builder works as usual | no |
|
||||
| `va` | hidden | **hidden** - filtering comes from the report instead | yes |
|
||||
| `false` (or omitted) | shown | shown | no |
|
||||
|
||||
The header and navigation means the Data Controller title bar (with its user menu), the back button and the sub-nav - everything Data Controller would normally show around the grid.
|
||||
|
||||
`embed=true` hides that and nothing else: the editor behaves exactly as it does outside a report, including the Filter button, Add Record, and any filtering you apply yourself.
|
||||
|
||||
`embed=va` hides the same furniture **and** hands these over to the report:
|
||||
|
||||
- **filtering** - the report's filters are captured and applied as selections change, in either live mode or confirm mode (see Filter Modes below). The first table load is already filtered, so there is no unfiltered flash.
|
||||
- **column visibility** - the grid shows the columns the report sends, controlled from the report's Edit Report interface; primary-key columns are always kept visible.
|
||||
- **the Filter button is not offered** - filtering belongs to the report in this mode.
|
||||
- **Add Record is not offered** - rows are still added from Edit mode's "Add Row".
|
||||
- **a "Back to Edit table" bar** appears on the stage, approve and review pages, so a user who follows the review workflow can return to the editor without knowing Data Controller's page paths.
|
||||
|
||||
The two filters are not combined. A filter built inside Data Controller and a filter driven by the report are alternatives, not layers: applying one replaces the other, and in `va` mode the report's filter is re-applied the next time the selection changes. Showing the button there would offer a filter that silently stops applying as soon as the report is touched, which is why it is hidden rather than disabled.
|
||||
|
||||
## How VA drives the editor
|
||||
|
||||
VA pushes a JSON payload to the iframe via `window.postMessage` whenever the selected rows change. Data Controller listens for these messages and:
|
||||
|
||||
1. Resolves each VA `parameter` to a DC column by matching on column label.
|
||||
2. Builds a filter (using existing [Filter](/filter/) machinery) over the selected values.
|
||||
3. Hides any VA columns marked as `brush` so the grid stays focused on the user-editable columns (primary key columns are always shown).
|
||||
|
||||
If VA sends an empty / unmatched message the editor falls back to the unfiltered view but stays in VA mode.
|
||||
|
||||
In the report below, two report parameters - `SOME_DROPDOWN` and `SOME_DATE` - reach one Data Controller object, and the editor shows both the combined filter and the rows it matches:
|
||||
|
||||

|
||||
|
||||
More logic available in [`va-messaging.service.ts`](https://git.datacontroller.io/dc/dc/src/branch/main/client/src/app/services/va-messaging.service.ts).
|
||||
|
||||
## Mapping parameters to columns
|
||||
|
||||
By default each VA `parameter` is matched to a DC column by name: a parameter whose label is a column of the table filters that column. That means two DC instances in the same report can only filter the same column from a parameter of the same name - so two report tabs cannot hold different filters on one column.
|
||||
|
||||
The `va_parmap` URL parameter overrides the name match, one entry per mapping, `<VA parameter>-<DC column>`:
|
||||
|
||||
```text
|
||||
https://yourserver/DC/#/editor/<library>/<table>?embed=va&va_parmap=REGION_TAB1-REGION
|
||||
```
|
||||
|
||||
Repeat the parameter to add mappings. A report with two tabs, each holding its own instance of the same table and filtering the same column from its own report parameter:
|
||||
|
||||
| Report tab | DDC URL |
|
||||
|---|---|
|
||||
| Tab 1 | `...?embed=va&va_parmap=REGION_TAB1-REGION` |
|
||||
| Tab 2 | `...?embed=va&va_parmap=REGION_TAB2-REGION` |
|
||||
|
||||
The applied filter is visible in the editor, above the grid - here the parameter `DROPDOWN_TAB1` filtering `SOME_DROPDOWN`, which is not its name:
|
||||
|
||||

|
||||
|
||||
The mapping is per parameter, and additive:
|
||||
|
||||
- a mapped parameter always filters the column it names; that column's type in the table decides how the value is formatted, exactly as for a name match
|
||||
- a parameter with no mapping keeps the default name match, so a single instance can mix mapped and unmapped parameters
|
||||
- the key is matched case-insensitively against the parameter's label
|
||||
- an entry is split at its last `-`, so a label may itself contain hyphens (`Sales-2024-DEPT` maps the label `Sales-2024` to `DEPT`)
|
||||
- if the named column is not in the table being opened, the parameter is ignored rather than falling back to the name match - a typo cannot silently filter the wrong column
|
||||
- a column name that is not a plain SAS name is ignored
|
||||
- the mapping applies to filtering only; which columns the grid displays still follows the columns VA sends
|
||||
|
||||
## When a mapping cannot be used
|
||||
|
||||
A wrong mapping would otherwise look like a working filter over the whole table, so the editor puts a banner above the grid naming the parameter, the column, the table, and what to change:
|
||||
|
||||

|
||||
|
||||
Each line names one cause, and the parameter it concerns is skipped rather than falling back to the name match - a typo never silently filters the wrong column:
|
||||
|
||||
| What the banner says | What it means |
|
||||
|---|---|
|
||||
| `is not a column of <libref>.<table>` | the mapped column does not exist in the table being opened |
|
||||
| `is not a date or datetime column` | a date anchor (`ADD`/`RANGE`) was applied to a column that holds something else |
|
||||
| `VA is not sending` | no message has named that parameter - usually a spelling mistake in the parameter side of the entry |
|
||||
| `all filter <COLUMN> ... different values will show no rows` | two parameters map to one column with different values, so the AND can never match |
|
||||
| `a range needs one value` | a `RANGE` mapping was sent several values - one `BETWEEN` carries one period |
|
||||
| `N of its M values was not a date` | part of a multi-value parameter could not be read as a date, so only the rest is applied |
|
||||
| `was ignored: it needs a "-"` / `is not a transform` | the entry itself is malformed |
|
||||
| `the VA parameter name is missing` | the entry starts with the `-`, so there is no parameter to map |
|
||||
| `already maps to <COLUMN>, and a parameter can filter only one column` | the same parameter is mapped twice; the first entry wins |
|
||||
|
||||
The banner is shown to the report author, inside the report, because that is where the entry is edited: the only fix available from the editor is to change the `va_parmap` entries in the URL of the Data Controller object.
|
||||
|
||||
!!! note
|
||||
A parameter label containing `&` must be percent-encoded (`%26`), because `&` separates query parameters. A space may be written as `+` or `%20`.
|
||||
|
||||
## Deriving dates from a parameter value
|
||||
|
||||
A mapping can resolve the parameter value through a **named date anchor** before the filter is built, by appending a transform to the column name:
|
||||
|
||||
```text
|
||||
https://yourserver/DC/#/editor/<library>/<table>?embed=va&va_parmap=SELECTED_DATE-D_DATE:ADD(QUARTER_START,-1)
|
||||
```
|
||||
|
||||
A report that sends `SELECTED_DATE = 27FEB1961` then filters `D_DATE` on 1961-02-27 **and** 1960-10-01 - the selected date, plus the same point one quarter earlier:
|
||||
|
||||
In the screenshot below the mapping is `SELECTED_DATE-SOME_DATE:ADD(QUARTER_START,-1)` against the demo table, and the parameter sends 27FEB1961:
|
||||
|
||||

|
||||
|
||||
The clause carries both values, so the row for the selected date and the row for the previous quarter are both included.
|
||||
|
||||
| Transform | Result | Filter |
|
||||
|---|---|---|
|
||||
| `ADD(<anchor>,<offset>)` | the value, plus one anchor beside it | `D_DATE IN (01OCT2026, 01JUL2026)` |
|
||||
| `RANGE(<period>,<offset>)` | one closed period | `D_DATE BETWEEN 01APR2026 AND 30JUN2026` |
|
||||
|
||||
The anchors are `QUARTER_START`, `QUARTER_END`, `MONTH_START`, `MONTH_END`, `YEAR_START` and `YEAR_END`; the periods are `QUARTER`, `MONTH` and `YEAR`. The offset is a whole number of periods - negative for earlier - and defaults to `0`. Names and arguments are case-insensitive.
|
||||
|
||||
```text
|
||||
va_parmap=SELECTED_DATE-D_DATE:ADD(QUARTER_START,-1)
|
||||
va_parmap=SELECTED_DATE-D_DATE:ADD(MONTH_END,-1)
|
||||
va_parmap=SELECTED_DATE-D_DATE:RANGE(QUARTER,-1)
|
||||
va_parmap=SELECTED_DATE-D_DATE:ADD(YEAR_START,-1)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- the arithmetic is anchored on the parameter's value, never on today, so a saved report selects the same rows whenever it is opened
|
||||
- the result is always a single filter clause, so a transform combines with other parameters exactly as an unmapped one does
|
||||
- a transform only applies to date and datetime columns; on any other column the parameter is ignored and the warning banner says so
|
||||
- the offset is a whole number; unknown anchors, periods and operations are rejected with a warning naming the entry
|
||||
|
||||
## Filter Modes
|
||||
|
||||
When running in `embed=va` mode, the editor provides two filter modes, controlled by the **Auto-apply** checkbox:
|
||||
|
||||
* **Live (default)** - the editor updates automatically as you select rows in the VA report, so the data you see always matches your current selection.
|
||||
* **Confirm** - filter changes are held until you click the **Apply** button. This is useful when editing, where an automatic reload would discard unsaved changes.
|
||||
|
||||
A status indicator shows whether a filter change is pending, loading, or idle. In edit mode, a pending filter is held until you leave edit mode, so your unsaved edits are never lost.
|
||||
|
||||
## Configuration in VA
|
||||
|
||||
The mapping lives entirely in the URL of the report object, so the job is to get the right URL onto a **Data-driven content** object and make sure the parameters it names actually reach it.
|
||||
|
||||
1. **Add the object.** In the VA Report Designer, insert a **Data-Driven Content** object, or select the one already on the page.
|
||||
|
||||
2. **Point it at Data Controller.** Open **Options** on the right and paste the SAS Job Execution URL of your DC deployment into **Web Content URL**, with `embed=va` and one `va_parmap` entry per parameter:
|
||||
|
||||
```text
|
||||
https://yourserver/SASJobExecution/?_file=/YOUR/APPLOC/services/DC.html#/editor/YOURLIB.YOURTABLE?embed=va&va_parmap=REGION_TAB1-REGION
|
||||
```
|
||||
|
||||
Everything before the `#` is the normal deployment URL; the `#/editor/...` part is the DC route, and `?embed=va` plus the `va_parmap` entries are what this page describes. One object per report tab, each with its own entry, is what lets two tabs filter the same column differently.
|
||||
|
||||
The URL is a property of the object, not a data source: it lives in **Options > Web Content > URL**, and the object needs no data assigned for Data Controller to open its own table. In the designer that section looks like this (the field is scrolled to the end of a long URL):
|
||||
|
||||

|
||||
|
||||
3. **Make the parameter reach the object.** VA sends an object only the parameters its query uses. A report parameter that the object's data does not reference never arrives, and the mapping then has nothing to map - DC reports it as `VA is not sending ...` in the banner. So bind the parameter into the object (assign it as a source on the object's query, as you would for any other visual), rather than only defining it at report level.
|
||||
|
||||
4. **Save the report**, then open it. DC shows the applied filter above the grid, so a correct mapping is visible without opening the editor:
|
||||
|
||||
```text
|
||||
FILTER : ( ( REGION = 'Europe' ) )
|
||||
```
|
||||
|
||||
A date parameter arrives as a display date and is rendered as a SAS date literal, the same as a date filter built inside Data Controller:
|
||||
|
||||
```text
|
||||
FILTER : ( ( SOME_DROPDOWN = 'Option 1' AND SOME_DATE BETWEEN '01JAN1960'd AND '31MAR1960'd ) )
|
||||
```
|
||||
|
||||
Something wrong shows the banner instead, with the cause and the fix - see [When a mapping cannot be used](#when-a-mapping-cannot-be-used) above.
|
||||
|
||||
!!! note
|
||||
The URL is read when the object loads, so a change to `va_parmap` takes effect when the report (or the page holding the object) is next opened.
|
||||
|
||||
### Who the object runs as
|
||||
|
||||
The object runs as the person **viewing** the report, using their SAS identity - there is no service account behind it. That person must therefore be a registered Data Controller user: if they are not, DC shows its login card inside the object instead of the editor, which looks like a broken embed rather than a permissions matter.
|
||||
|
||||
Register everyone who will open the report before you ship it (Users in DC), and test the report as one of them rather than as the person who built it - the author often has elevated permissions, so a report that works for them can fail for everyone else.
|
||||
|
||||
The deployment itself has to be licensed and past its End User License Agreement before it is embedded, for the same reason: an unlicensed instance shows the licensing screen (the agreement, then the free tier until a key is uploaded) inside the object, which reads as a broken embed rather than as setup that was never finished. Accept the agreement and register the key by opening the deployment directly, then reload the report.
|
||||
|
||||
## Debugging
|
||||
|
||||
A hook script can tell an embedded editor from a standalone one without any client-side inspection: `work.browser_info` carries the page URL and `referrer`, and `work.browser_url_vars` carries `embed=va`. See [Browser Variables](/developer-guide/browser-variables/).
|
||||
|
||||
The following snippet can be used in console to dump the values being provided to DC from VA:
|
||||
|
||||
```
|
||||
console.log(JSON.stringify(window.__vaLastMessage?.data, null, 2))
|
||||
```
|
||||
|
||||
Each entry in its `parameters` array carries a `label`, and that label is what a `va_parmap` entry has to match (and what the name match uses when there is no entry). When a mapping appears to do nothing, compare the labels in this dump with the left-hand side of the entries in the object's URL - a parameter that VA labels differently from what was typed in the URL is silently a different parameter.
|
||||
@@ -14,6 +14,10 @@ nav:
|
||||
- Table Viewer: dcu-tableviewer.md
|
||||
- ViewBoxes: viewboxes.md
|
||||
- Admin Services: admin-services.md
|
||||
- Developer Guide:
|
||||
- Filter in a Pre-Edit Hook: developer-guide/filter-pre-edit-hook.md
|
||||
- One Table, Two Rule Sets: developer-guide/two-rule-sets-one-table.md
|
||||
- Browser Variables: developer-guide/browser-variables.md
|
||||
- Table Guide:
|
||||
- MPE_AUDIT: tables/mpe_audit.md
|
||||
- MPE_COLUMN_LEVEL_SECURITY: tables/mpe_column_level_security.md
|
||||
@@ -29,6 +33,7 @@ nav:
|
||||
- MPE_DATASTATUS_TABS: tables/mpe_datastatus_tabs.md
|
||||
- MPE_DATADICTIONARY: tables/mpe_datadictionary.md
|
||||
- MPE_DATALOADS: tables/mpe_dataloads.md
|
||||
- MPE_DERIVED: tables/mpe_derived.md
|
||||
- MPE_EMAILS: tables/mpe_emails.md
|
||||
- MPE_EXCEL_CONFIG: tables/mpe_excel_config.md
|
||||
- MPE_FILTERANYTABLE: tables/mpe_filteranytable.md
|
||||
@@ -55,6 +60,7 @@ nav:
|
||||
- CAS Tables: cas-tables.md
|
||||
- Column Level Security: column-level-security.md
|
||||
- Dates / Datetimes: dcc-dates.md
|
||||
- Derived Variables: derived-variables.md
|
||||
- Dynamic Cell Dropdown: dynamic-cell-dropdown.md
|
||||
- Emails: emails.md
|
||||
- Excel: excel.md
|
||||
@@ -67,7 +73,7 @@ nav:
|
||||
- Selectboxes: dcc-selectbox.md
|
||||
- Tables: dcc-tables.md
|
||||
- Validations: dcc-validations.md
|
||||
- Visual Analytics: embed-va.md
|
||||
- Visual Analytics: visual-analytics.md
|
||||
- Macros: macros.md
|
||||
- Installation:
|
||||
- System Requirements: dci-requirements.md
|
||||
@@ -107,6 +113,7 @@ plugins:
|
||||
redirect_maps:
|
||||
'dci-evaluation.md': 'dci-deploysas9.md'
|
||||
'dcu-fileupload.md': 'files.md'
|
||||
'embed-va.md': 'visual-analytics.md'
|
||||
|
||||
# Repository
|
||||
repo_name: 'dc/docs.datacontroller.io'
|
||||
|
||||