Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
094e715f5d | ||
|
|
59ff2b13fc | ||
|
|
7d950cbdbe | ||
|
|
66d5fecb0e | ||
|
|
0b18658686 | ||
|
|
bbd5793d8b | ||
|
|
0bda92b091 | ||
|
|
bf9fca1b9a | ||
|
|
d542ac442f | ||
|
|
0ee0b4ea7c | ||
|
|
2fe1bc2eea | ||
|
|
3e39339332 | ||
|
|
6d554930be | ||
|
|
a56c2f8d48 | ||
|
|
27ff40b739 | ||
|
|
4937edd7f4 | ||
|
|
fcba390804 | ||
|
|
8b6e3e60bc | ||
|
|
d244a34fc9 | ||
|
|
686a745f0b | ||
|
|
510947d173 | ||
|
|
589a745c5b | ||
|
|
d04c55c731 | ||
|
|
a6a6abf009 | ||
|
|
88368fa31d | ||
|
|
fdb1db216d | ||
|
|
6d833f9654 | ||
|
|
6bcd0adf81 | ||
|
|
93348ad604 | ||
|
|
7561bb0c39 | ||
|
|
959a9ffab4 | ||
|
|
2348575737 | ||
|
|
b2aac21b90 | ||
|
|
438b1e93d7 | ||
|
|
674737f840 | ||
|
|
150be89246 | ||
|
|
b6fea27963 | ||
|
|
aa1f015f5a | ||
|
|
79b62e4cfc | ||
|
|
73d4796638 | ||
|
|
7267af58c4 | ||
|
|
e5dfcd84da | ||
|
|
02295334b3 | ||
|
|
e7a538ed9a | ||
|
|
60a19dda89 | ||
|
|
b415793eec | ||
|
|
d352ea8fea | ||
|
|
9e04cd4220 | ||
|
|
181c3ae834 | ||
|
|
0ec0789ac4 | ||
|
|
2e5fc4a368 | ||
|
|
3d66f91820 | ||
|
|
5cdfadf75c | ||
|
|
fef2c3c12e |
No files matched your search
@@ -14,7 +14,6 @@ jobs:
|
|||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci --legacy-peer-deps
|
run: npm ci --legacy-peer-deps
|
||||||
|
|||||||
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
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.'
|
||||||
|
date: '2026-09-24 15:30:00'
|
||||||
|
author: 'Data Controller'
|
||||||
|
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||||
|
tags:
|
||||||
|
- Data Quality
|
||||||
|
- Configuration
|
||||||
|
previewImg: './rule-set-1.png'
|
||||||
|
---
|
||||||
|
|
||||||
|
# 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](#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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### The detail that will bite you
|
||||||
|
|
||||||
|
Use `call symputx`, not `%let`. `LIBREF` and `DS` are not declared `%local` in `mpe_loader`, and the hook is included into that scope - so `call symputx` finds the existing variable and updates it, while a `%let` creates a new variable in the hook's own scope and is silently discarded. The hook runs, the log looks clean, and the changeset goes to the mirror anyway.
|
||||||
|
|
||||||
|
### 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` cap will stop the edit screen with "Table is too big".
|
||||||
|
- 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.
|
||||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -61,7 +61,7 @@ Did you know that, in addition to a regular missing value in SAS (`.`), there ar
|
|||||||
|
|
||||||
These values can now be both viewed and edited in Data Controller following an update to the [SASjs Adapter](https://github.com/sasjs/adapter#variable-types).
|
These values can now be both viewed and edited in Data Controller following an update to the [SASjs Adapter](https://github.com/sasjs/adapter#variable-types).
|
||||||
|
|
||||||
`video: [Retain Formulas when Loading Excel to SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
`video: [Managing Special Missing Values with Data Controller for SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
||||||
|
|
||||||
There is nothing extra to configure for special SAS numerics - they are simply available by default, for numeric cells.
|
There is nothing extra to configure for special SAS numerics - they are simply available by default, for numeric cells.
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 57 KiB |
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
title: "v7.13 Release: Formulas & Regex"
|
||||||
|
description: Data Controller 7.13 wires a spreadsheet-grade formula engine into the editor grid, completing the point-of-entry validation story that began with 7.12's regex rules. Both are pure MPE_VALIDATIONS configuration - no code, no deployment.
|
||||||
|
date: '2026-09-03 12:00:00'
|
||||||
|
author: 'Allan Bowe'
|
||||||
|
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||||
|
previewImg: './v713_cover.png'
|
||||||
|
tags:
|
||||||
|
- Releases
|
||||||
|
- Data Controller
|
||||||
|
---
|
||||||
|
|
||||||
|
# v7.13: Formulas & Regex
|
||||||
|
|
||||||
|
We stopped writing release-specific blog posts after v6.1 - the automated, public release system made them redundant for routine version bumps. But every so often a release lands that changes what the product can actually _do_, and v7.13 is one of them. Together with 7.12 it completes a piece of the [roadmap](https://docs.datacontroller.io/roadmap/) we have been chipping away at for a while: **frontend formulae and regex rules**, both configurable purely in `MPE_VALIDATIONS`.
|
||||||
|
|
||||||
|
If you configure data entry in Data Controller, this post is a practical guide: how the rules work, how to set them up, and the process flow from config to grid. All the screenshots below are captured from a live editor session against the demo tables, so what you see is what shipped.
|
||||||
|
|
||||||
|
## Why this matters
|
||||||
|
|
||||||
|
Data Controller's job is to let business users change data safely. A big part of "safely" is catching problems at the point of entry - rather than after an approval, in a batch log, or (worst case) in a report. The [validations](https://docs.datacontroller.io/dcc-validations/) framework already covered length, type, nullability, primary keys, ranges, casing and dropdowns. Two things were missing:
|
||||||
|
|
||||||
|
* **Computed values.** Plenty of tables have columns that are derived from other columns - a revenue column that is price times volume, a stamp column that records who last touched a row. Until now the options were a backend hook script (real SAS code to write, test and deploy) or just letting users type anything.
|
||||||
|
* **Pattern enforcement.** A dropdown is overkill when all you need is "this value looks like an email address" or "this postcode is well-formed". You want the _shape_ of the value checked as typed - and sometimes you want a hard block, sometimes just a gentle warning.
|
||||||
|
|
||||||
|
7.12 delivered regex rules (`HARDREGEX` / `SOFTREGEX`). 7.13 delivers formulas (`HARDFORMULA` / `SOFTFORMULA`). Both are rows in a config table. That is the whole feature.
|
||||||
|
|
||||||
|
## The process flow: from MPE_VALIDATIONS to the grid
|
||||||
|
|
||||||
|
Every configurable rule in Data Controller follows the same pipeline, and the new rules plug straight into it:
|
||||||
|
|
||||||
|
1. **Configure.** `MPE_VALIDATIONS` is itself a Data Controller table - open it from the navigation tree like any other, and add a row with `BASE_LIB`, `BASE_DS` and `BASE_COL` pointing at the column you want to govern, `RULE_TYPE` set to the new rule, `RULE_VALUE` holding the formula or pattern, and `RULE_ACTIVE=1`. Submit, approve, done - it's a config change, not a code release. The [MPE_VALIDATIONS table guide](https://docs.datacontroller.io/tables/mpe_validations/) has the full column reference.
|
||||||
|
2. **Serve.** When a user opens the editor, the [`editors/getdata`](https://code.datacontroller.io/getdata_8sas.html) service extracts the active rules for that table - it filters `MPE_VALIDATIONS` on library, table and `RULE_ACTIVE=1` - and returns them in the `dqrules` object of the response, alongside the table data, schema, and the schema-derived `NOTNULL` constraints.
|
||||||
|
3. **Apply.** The frontend wires each rule into the Handsontable grid: formula rules are computed live by [HyperFormula](https://hyperformula.handsontable.com/) (the calculation engine behind Handsontable itself), regex rules are evaluated in the browser with the JavaScript regex engine.
|
||||||
|
4. **Block or warn.** On submit, the standard [cell validation](https://docs.datacontroller.io/dcc-validations/) cycle runs: `HARD` rules block the submission if violated, `SOFT` rules warn but allow.
|
||||||
|
|
||||||
|
Here are the new rule types as they appear in the config - one row per rule, `RULE_VALUE` holding the formula or the pattern:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For regex there is also a config-time guard, and it's a neat piece of dogfooding: because the rule itself lives in a table, saving an edit to `MPE_VALIDATIONS` through Data Controller runs the [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html), which passes every `HARDREGEX` / `SOFTREGEX` `RULE_VALUE` through `PRXPARSE` and rejects the edit if the pattern is invalid - listing the offending columns. A typo in a pattern is caught the moment you save the rule, not the first time a user hits it.
|
||||||
|
|
||||||
|
## Regex rules (HARDREGEX / SOFTREGEX, v7.12)
|
||||||
|
|
||||||
|
Regex rules validate cell values against a SAS (Perl-style) regular expression. You provide the pattern in `RULE_VALUE`; whether it blocks or warns depends on the rule type:
|
||||||
|
|
||||||
|
* **HARDREGEX** - the value **must** match the pattern. If it doesn't, the cell is highlighted red and submission is blocked.
|
||||||
|
* **SOFTREGEX** - a non-matching value is highlighted yellow as a warning. The user can still submit - it's a nudge, not a block.
|
||||||
|
|
||||||
|
### Setting one up
|
||||||
|
|
||||||
|
Say `SOME_CHAR` in the demo table must contain either "the" or "data" (case-insensitive). That's one insert:
|
||||||
|
|
||||||
|
```sas
|
||||||
|
insert into &lib..MPE_VALIDATIONS set
|
||||||
|
tx_from=0
|
||||||
|
,base_lib="&lib"
|
||||||
|
,base_ds="MPE_X_TEST"
|
||||||
|
,base_col="SOME_CHAR"
|
||||||
|
,rule_type='HARDREGEX'
|
||||||
|
,rule_value='/the|data/i'
|
||||||
|
,rule_active=1
|
||||||
|
,tx_to='31DEC5999:23:59:59'dt;
|
||||||
|
```
|
||||||
|
|
||||||
|
That's a real row from the demo data, by the way - the shipped `MPE_X_TEST` table carries sample `HARDREGEX` and `SOFTREGEX` rules so you can try both without touching your own config.
|
||||||
|
|
||||||
|
In the editor it looks like this - an invalid email blocked red by `HARDREGEX`, an invalid postcode warned yellow by `SOFTREGEX`, and a value that passes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Note the third column in that screenshot, `REGEX_BOTH_COL`. It carries _both_ a `HARDREGEX` and a `SOFTREGEX` rule - and shows neither warning. That's the precedence rule in action: only one regex is ever applied per column, and if both exist the `SOFTREGEX` is ignored entirely, so the column behaves exactly as if it were `HARDREGEX`-only.
|
||||||
|
|
||||||
|
### What to know before writing patterns
|
||||||
|
|
||||||
|
The full list of gotchas is in the [regex rules documentation](https://docs.datacontroller.io/dcc-validations/#regex-rules); the ones that matter most in practice:
|
||||||
|
|
||||||
|
* **Use the PRX delimiter form.** Patterns are authored exactly as [PRXPARSE](https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/lefunctionsref/p0s9ilagexmjl8n1u7e1t1jfnzlk.htm) accepts them: `/pattern/flags`, e.g. `/^\d+$/` for "integers only", `/the|data/i` for a case-insensitive contains. The config-time `PRXPARSE` check enforces this - a bare pattern without delimiters will be rejected when you save the rule. (The frontend tolerates bare patterns for backwards compatibility, but don't author new rules that way.)
|
||||||
|
* **Anchor your own patterns.** The pattern is used **as authored** - it is not auto-anchored. `/the|data/i` matches "the" _anywhere_ in the value. If you want the whole value to match, include `^` and `$` yourself.
|
||||||
|
* **Stick to the common subset.** The pattern is evaluated in the browser with the JavaScript regex engine, which shares SAS PRX's core syntax (character classes, quantifiers, groups, alternation, `^`/`$` anchors, `\d \w \s` and friends). Three unambiguous Perl-isms are translated automatically - a leading `(?i)` modifier, `\Q...\E` literal sequences, and `\A`/`\z` absolute anchors. But Perl-only constructs such as possessive quantifiers (`a++`) and atomic groups (`(?>...)`) pass the SAS-side check and then silently do nothing in the frontend - so just don't use them.
|
||||||
|
* **Blank is exempt.** Blank values skip pattern matching on any column type (use the `NOTNULL` rule if you also need populated values). On numeric columns the plain SAS missing (`.`) is also exempt - but special missings (`.A`-`.Z`, `._`) are not: they're deliberately-set values, so your pattern needs to accommodate them.
|
||||||
|
* **One regex per column.** As above - `HARDREGEX` wins when both are present, and the column-header info dropdown shows only the rule that is actually applied.
|
||||||
|
* **Length limit.** `RULE_VALUE` is 128 characters, which constrains very long patterns.
|
||||||
|
* **Deleted rows are exempt.** Cells in rows marked for deletion are not validated or warned (except primary key columns, which still are).
|
||||||
|
|
||||||
|
### Where regex beats a dropdown
|
||||||
|
|
||||||
|
For currency codes, country codes, account number formats, email shapes, or "must be an integer" - a regex is one row of config versus a 1000-value `HARDSELECT` dropdown. And unlike a dropdown, a regex catches _paste_ operations too, not just typed values.
|
||||||
|
|
||||||
|
## Formula rules (HARDFORMULA / SOFTFORMULA, v7.13)
|
||||||
|
|
||||||
|
Formula rules make a column compute itself from other columns in the same row - like a spreadsheet formula, but the formula lives in config and applies to every row. When a user opens the editor, the formula is evaluated live and the result is shown in each cell. Change an input, and every dependent cell recalculates on the spot.
|
||||||
|
|
||||||
|
### Writing a formula
|
||||||
|
|
||||||
|
Formulas use **column names, not cell references** - there is no need to know the grid layout. If you have `A_COL` and `B_COL`, a `FORMULA_HARD_COL` rule is simply:
|
||||||
|
|
||||||
|
```
|
||||||
|
=A_COL * B_COL
|
||||||
|
```
|
||||||
|
|
||||||
|
Each row calculates its own result: row 1's values, row 2's values, and so on. Under the hood each column name is translated to a row-relative cell reference (text inside quotes is left untouched) and handed to HyperFormula - so you get the full spreadsheet function library, `IF`, `SUM`, `ROUND`, `CONCAT` and friends, without writing any code.
|
||||||
|
|
||||||
|
The one syntax rule: **each column name must be surrounded by spaces**. `=MATCH( PRICE )` resolves the column reference; `=MATCH(PRICE)` does not - without the surrounding blanks the token isn't recognised as a column reference, and the formula errors rather than using the column's value. The spaces stop column names clashing with function names.
|
||||||
|
|
||||||
|
### HARD vs SOFT formulas
|
||||||
|
|
||||||
|
* **HARDFORMULA** - the column is read-only. The formula result is always shown and submitted; the user cannot change it. Think calculated amounts, or a `PROCESSED_BY` column.
|
||||||
|
* **SOFTFORMULA** - the cell shows the formula result, but the user can type a different value if the computed one is wrong. Their value is submitted instead. Useful for derived defaults where the business occasionally needs to override.
|
||||||
|
|
||||||
|
### Special values
|
||||||
|
|
||||||
|
Formulas can reference three runtime values, resolved when the formula is evaluated:
|
||||||
|
|
||||||
|
* `DC.ROW_STATUS` - the row's current state: `M` (Modified), `A` (Added), `D` (Deleted), or `U` (Unchanged). A newly-added row is `A` from the moment it is created - there is no transient state before that.
|
||||||
|
* `DC.USER_NAME` - the logged-in user id.
|
||||||
|
* `DC.ORIG_VALUE` - the original cell value before the current edit.
|
||||||
|
|
||||||
|
Our demo formula table puts them all to work. There's a row-status column (`=DC.ROW_STATUS`), a user column (`=DC.USER_NAME`), and a `CHANGE_SUMMARY_COL` whose rule reads like a proper audit sentence:
|
||||||
|
|
||||||
|
```
|
||||||
|
=IF( DC.ROW_STATUS ="U","unedited",
|
||||||
|
DC.USER_NAME &" changed from "& DC.ORIG_VALUE )
|
||||||
|
```
|
||||||
|
|
||||||
|
Here it is live in the editor. We edited `B_COL` on the second row from 10 to 25 - and the whole row reacted: `FORMULA_HARD_COL` recomputed to 50 (`A_COL * B_COL`), `FORMULA_SOFT_COL` recomputed to 27 (`A_COL + B_COL`), and the row status flipped from `U` to `M` - all instantly, all without touching a single line of SAS:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
And here's the payoff of those `DC.*` references - the audit columns from the same table, same edit. `ROW_STATUS_COL` flipped to `M`, and the change summary resolved the `IF` formula above into a sentence - "sasdemo changed from orig-2":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Because `DC.ROW_STATUS` is a live reference, it updates as the user works: edit a cell and the stamp flips to your user id; cancel the edit and it reverts.
|
||||||
|
|
||||||
|
### Editor behaviour worth knowing
|
||||||
|
|
||||||
|
* When you paste a formula into the grid, column names are automatically translated so the formula works in its new position.
|
||||||
|
* A cell that is overwritten by a formula is flagged (with the original value retained) so you can revert it.
|
||||||
|
* Formula-looking values pasted from Excel are treated as plain data (not evaluated), unless you explicitly choose "Apply as formula" - a deliberate safety measure so a spreadsheet's internal formulas don't leak into your data as live rules.
|
||||||
|
* When submitted, it's the formula's computed value that is sent to the backend, never the raw formula text (a primary key column always resolves its live formula immediately to the computed value so the submission keys are correct).
|
||||||
|
|
||||||
|
## How it works under the hood (briefly)
|
||||||
|
|
||||||
|
Two moving parts, and the boundary between them explains most of the gotchas above.
|
||||||
|
|
||||||
|
**Serving the rules.** [`getdata`](https://code.datacontroller.io/getdata_8sas.html) extracts the active `MPE_VALIDATIONS` rows for the target table and returns them in `dqrules`, along with the schema-derived `NOTNULL` constraints. Formulas and regex are frontend rules - they are evaluated in the browser, not in SAS - which is why they arrive as `dqrules` rather than as backend hook scripts.
|
||||||
|
|
||||||
|
**Evaluating them.** For formulas, the client translates each column name in `RULE_VALUE` to a row-relative cell reference and hands it to HyperFormula (wired into Handsontable's formulas plugin). For regex, the client parses the PRX `/pattern/flags` form, translates the three Perl-isms it can, and constructs a JavaScript `RegExp`. If a pattern still fails in the browser, the editor treats it as always-valid rather than breaking - a failed pattern never blocks a submission it shouldn't.
|
||||||
|
|
||||||
|
**Guarding the config.** Because `MPE_VALIDATIONS` is itself a Data Controller table, a [post-edit hook](https://code.datacontroller.io/mpe__validations__postedit_8sas.html) validates new rules: `PRXPARSE` checks every regex `RULE_VALUE`, and the edit is rejected with the offending columns listed. Invalid rules never reach users.
|
||||||
|
|
||||||
|
## Also in these releases
|
||||||
|
|
||||||
|
Beyond the headline features, the usual spread of hardening and polishing shipped along the way: row-header status cells are now colour-coded (with a `±` symbol for modified rows), CAS support landed for the `REPLACE` load type, Viya deploy diagnostics were improved, and a large tranche of dependency upgrades (Angular 20, Handsontable 18 pinned, sasjs core v5) keeps the audit trail clean. As ever, the full commit-by-commit detail is in the [release notes](https://git.datacontroller.io/dc/dc/releases).
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
The frontend changes are included in the 7.13 release assets. The backend additions are **data-only, optional migrations**:
|
||||||
|
|
||||||
|
* The **v7.12 migration** adds `HARDREGEX` / `SOFTREGEX` to the `RULE_TYPE` dropdown in `MPE_VALIDATIONS` (and switches the `MPE_SECURITY.LIBREF` validation to a hook that lists all libraries).
|
||||||
|
* The **v7.13 migration** adds `HARDFORMULA` / `SOFTFORMULA` to the same dropdown.
|
||||||
|
|
||||||
|
Both scripts are in [`sas/sasjs/db/migrations/`](https://git.datacontroller.io/dc/dc/src/branch/main/sas/sasjs/db/migrations) in the source repo, and they're worth running even if you don't plan to use the rules immediately - they only add dropdown values to `MPE_SELECTBOX`.
|
||||||
|
|
||||||
|
## Try it yourself
|
||||||
|
|
||||||
|
The shipped demo data includes the regex rules on `MPE_X_TEST`, so you can see them working without configuring anything: open the demo library, edit `MPE_X_TEST`, and try entering a `SOME_SHORTNUM` between 1 and 5 (blocked red - `HARDREGEX`), a `PRIMARY_KEY_FIELD` with a decimal point (warned yellow - `SOFTREGEX`), or a `SOME_CHAR` without "the" or "data" in it (blocked - `HARDREGEX`).
|
||||||
|
|
||||||
|
For formulas, add a rule to one of your own tables - the `REVENUE = PRICE * VOLUME` example above is a two-minute configuration, and the `DC.*` special values make audit-style columns almost free. Full reference in the [validations docs](https://docs.datacontroller.io/dcc-validations/).
|
||||||
|
|
||||||
|
As ever - if you'd like to see additional validation types, [get in touch](https://datacontroller.io/pricing). The roadmap is customer-driven, and the validations list keeps growing.
|
||||||
|
|
||||||
|
<!-- Source LinkedIn post:
|
||||||
|
|
||||||
|
Every SAS team has a spreadsheet that "helps" load data into production.
|
||||||
|
|
||||||
|
You know the one. Someone built it years ago, nobody fully understands it, and it writes straight to a table it probably shouldn't.
|
||||||
|
|
||||||
|
What if that spreadsheet was wrong BEFORE anyone could hit send?
|
||||||
|
|
||||||
|
That's the idea behind Data Controller - a web app for SAS that lets business users change data safely. Every change is checked at the point of entry, goes for approval, and lands in a full audit trail. No new infrastructure - it runs on the SAS you already have.
|
||||||
|
|
||||||
|
The latest release adds two things that used to require a developer:
|
||||||
|
|
||||||
|
→ Formulas. Type a rule like = PRICE * VOLUME and the revenue column computes itself - spreadsheet-style, in the browser, live.
|
||||||
|
|
||||||
|
→ Pattern checks. "Must be a valid email." "Must be an integer." One row of config. Bad values are blocked or flagged before submission - not found in next month's report.
|
||||||
|
|
||||||
|
Both are configuration, not code. If you can fill in a table, you can set them up.
|
||||||
|
|
||||||
|
We wrote up how it works (with live screenshots of a bad email being caught red-handed):
|
||||||
|
|
||||||
|
Link in the comments 👇
|
||||||
|
|
||||||
|
#sas #dataquality #endusercomputing #excel #datagovernance
|
||||||
|
|
||||||
|
First comment: https://datacontroller.io/v7-13-formulas-and-regex/
|
||||||
|
-->
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 473 KiB |
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: 'The 5 Lines of Defence for Business Data Inputs'
|
||||||
|
description: How Data Controller for SAS® defends data quality for workflows deriving from business inputs - data model, permissions, validation checks, post edit hooks, and approvals.
|
||||||
|
date: '2026-08-27 09:00:00'
|
||||||
|
author: 'Allan Bowe'
|
||||||
|
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||||
|
tags:
|
||||||
|
- Announcements
|
||||||
|
previewImg: './five-lines-of-defence.jpeg'
|
||||||
|
---
|
||||||
|
|
||||||
|
How does Data Controller for SAS® defend data quality for data workflows deriving from business inputs?
|
||||||
|
|
||||||
|
**⚔️ 1st Line of Defence - Data Model ⚔️**
|
||||||
|
|
||||||
|
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
|
||||||
|
|
||||||
|
**⚔️ 2nd Line of Defence - Data Permissions ⚔️**
|
||||||
|
|
||||||
|
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled data ingestion.
|
||||||
|
|
||||||
|
**⚔️ 3rd Line of Defence - Validation Checks ⚔️**
|
||||||
|
|
||||||
|
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to SAS upload. See the [validation documentation](https://docs.datacontroller.io/dcc-validations/) for details.
|
||||||
|
|
||||||
|
**⚔️ 4th Line of Defence - Post Edit Hook ⚔️**
|
||||||
|
|
||||||
|
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL) using a [post edit hook](https://docs.datacontroller.io/dcc-tables/#post_edit_hook). A failure here results in immediate user feedback / change rejection.
|
||||||
|
|
||||||
|
**⚔️ 5th Line of Defence - Approval Step(s) ⚔️**
|
||||||
|
|
||||||
|
Changes are reviewed with [one or more approvals](https://docs.datacontroller.io/dcc-tables/) BEFORE being applied to the target database. A full audit trail is also maintained.
|
||||||
|
|
||||||
|
All functionality is ZERO-CODE, works on SAS Viya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a [community edition](https://datacontroller.io/pricing/) - free for unlimited users.
|
||||||
|
|
||||||
|
If you'd like to strengthen your own defences - [let's chat](https://datacontroller.io/contact/).
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Source LinkedIn post:
|
||||||
|
|
||||||
|
How does Data Controller for SAS® defend #DataQuality for #DataWorkflows deriving from Business Inputs?
|
||||||
|
|
||||||
|
⚔️ 1st Line of Defence - Data Model ⚔️
|
||||||
|
The interface will only accept inputs conforming to this model (columns, types, lengths, indexes, constraints etc). The schema determines the behaviour - eg, a date format results in a date picker.
|
||||||
|
|
||||||
|
⚔️ 2nd Line of Defence - Data Permissions ⚔️
|
||||||
|
Changes are made using a SAS System Account (eg `sassrv`). This means you can safely DENY write-access to data for business users, preventing un-controlled #DataIngestion.
|
||||||
|
|
||||||
|
⚔️ 3rd Line of Defence - Validation checks ⚔️
|
||||||
|
Additional validation checks (eg value ranges, specific patterns) can be configured to run at the point of data capture, prior to #SAS upload. The docs for these are here: https://lnkd.in/djtaGPsr
|
||||||
|
|
||||||
|
⚔️ 4th Line of Defence - Post Edit Hook ⚔️
|
||||||
|
Complex / customer specific validation can be deployed as SAS code, to run after every EDIT (prior to APPROVAL). A failure here results in immediate user feedback / change rejection.
|
||||||
|
|
||||||
|
⚔️ 5th Line of Defence - Approval Step(s) ⚔️
|
||||||
|
Changes are reviewed with one or more approvals BEFORE being applied to the target database. A full audit trail is also maintained.
|
||||||
|
|
||||||
|
All functionality is ZERO-CODE, works on #sasViya / EBI / SASjs Server, and applies to any database you have an ACCESS engine for. There is also a Community edition - free for unlimited users.
|
||||||
|
|
||||||
|
#datagovernance #sasadmin #saspartners
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Image prompt:
|
||||||
|
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: a medieval-castle motif reimagined as a data fortress - five concentric defensive walls (rendered as clean glowing ring segments, each a slightly different teal/green shade, subtly numbered 1-5) surrounding a central database cylinder that glows safely at the core. An arrow or data-packet stream approaches from the top, passing checkpoints in each wall: a schema/grid icon (wall 1), a padlock (wall 2), a checklist with ticks (wall 3), a code angle-brackets icon (wall 4), and a stamp/approval tick (wall 5). One red/orange invalid packet is shown bouncing off an outer wall. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly playful, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
|
||||||
|
|
||||||
|
Generated with: local Routstr node (see blog-dev/skills routstr-image-generation), model gemini-3.1-flash-lite-image, output 1424x736 JPEG, ~31 sats.
|
||||||
|
-->
|
||||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
title: 'Full Table Search: Find Any Value in Any Table'
|
||||||
|
description: Type a value into the search box and Data Controller scans every column for it - no query, no WHERE clause.
|
||||||
|
date: '2026-09-15 17:00:00'
|
||||||
|
author: 'Data Controller'
|
||||||
|
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||||
|
tags:
|
||||||
|
- Announcements
|
||||||
|
previewImg: './full-table-search.jpeg'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Full Table Search: Find Any Value in Any Table
|
||||||
|
|
||||||
|
Finding a value in a large table usually means writing a query first: guess which column it lives in, write a WHERE clause, run it, and try again when you guess wrong. Data Controller's **full table search** removes that step. Choose a library and table in the Viewer, type a value into the search box and press Enter - every column in the table is scanned for it, and the matching rows come straight back into the grid.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The search box sits in the Viewer toolbar, next to a **Numeric** checkbox:
|
||||||
|
|
||||||
|
- **Text search** matches part of a value using the case sensitive SAS® `CONTAINS` operator, so `smith` finds `Goldsmith` but not `Smithson`.
|
||||||
|
- **Numeric search** (tick the box) matches the number exactly against every numeric column in the table.
|
||||||
|
|
||||||
|
Whichever you use, the results are ordinary rows in the Viewer, with the rest of the screen behaving exactly as it does for a normal view.
|
||||||
|
|
||||||
|
## Any database, not just SAS datasets
|
||||||
|
|
||||||
|
The scan runs inside SAS, against whichever libname engine the table is assigned to - so it is not limited to SAS datasets.
|
||||||
|
|
||||||
|
## Built on open source
|
||||||
|
|
||||||
|
This feature, like most of Data Controller, is built on the [SASjs Macro Core](https://github.com/sasjs/core) library, using the [`%mp_searchdata()`](https://core.sasjs.io/mp__searchdata_8sas.html) macro. The macro assembles a single DATA step with one `OR` clause per column - a `CONTAINS` test for character columns, an equality test for numeric ones - and writes out only the matching records. It is MIT licensed, so you can read, test and audit the code that is running against your data.
|
||||||
|
|
||||||
|
A few things worth knowing:
|
||||||
|
|
||||||
|
- The search respects your current filter, Row Level Security and Column Level Security - it scans the view you are entitled to see, not the raw table.
|
||||||
|
- Full table search is available in ViewBoxes as well, so the related tables lined up beside your main grid can be searched the same way.
|
||||||
|
- Nothing is shipped to an external index or search service: the scan happens on your own SAS platform.
|
||||||
|
|
||||||
|
See it in action:
|
||||||
|
|
||||||
|
<iframe title="Full Table Search" width="560" height="315" src="https://vid.4gl.io/videos/embed/qdEv4PP2oiPr5VXwLbN58B" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||||
|
|
||||||
|
More detail in the [Viewer documentation](https://docs.datacontroller.io/dcu-tableviewer/).
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Source LinkedIn post:
|
||||||
|
|
||||||
|
Data Controller for SAS® has a plethora of features to make Data Discovery easier
|
||||||
|
|
||||||
|
Here we demonstrate "full table search". No need to formulate a query - just type a value and hit enter!
|
||||||
|
|
||||||
|
It works on all databases
|
||||||
|
|
||||||
|
This feature, like most others, is built on our open-source #SASjs library - using the `mp_searchdata()` macro (https://lnkd.in/gxZZq76j).
|
||||||
|
|
||||||
|
#sas #sasapps #sasviya
|
||||||
|
|
||||||
|
video: https://vid.4gl.io/w/qdEv4PP2oiPr5VXwLbN58B
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Image prompt:
|
||||||
|
Flat vector illustration on a dark slate (#314351) background with a faint dot grid: a stylised data table across the lower half (rounded header bar plus eight rows of rounded cells), with the fourth row highlighted in brand green (#90c445). A large magnifying glass with a thick green rim and diagonal handle overlaps the table on the right, and inside its dark lens the highlighted row appears magnified with two white value blocks. Headline "FULL TABLE SEARCH" in bold white uppercase, subheading "any value, every column - no query required" in brand green. 1200x627 landscape, no other text. Composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||||
|
-->
|
||||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: 'If: An Ode to Data Control'
|
||||||
|
description: Rudyard Kipling's 'If', reworked for the data governance era - the full text of a poem first published in 2022.
|
||||||
|
date: '2026-09-20 15:00:00'
|
||||||
|
author: 'Allan Bowe'
|
||||||
|
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||||
|
tags:
|
||||||
|
- Announcements
|
||||||
|
previewImg: './if-an-ode-to-data-control.jpeg'
|
||||||
|
---
|
||||||
|
|
||||||
|
# If: An Ode to Data Control
|
||||||
|
|
||||||
|
Rudyard Kipling wrote "If" in 1895 and published it in his 1910 collection Rewards and Fairies. This ode to data control was first published in 2022. Here is the full text.
|
||||||
|
|
||||||
|
```poem
|
||||||
|
If you can keep your data when all about you
|
||||||
|
Are losing theirs and blaming it on you;
|
||||||
|
If you can trust your metrics when analysts doubt you,
|
||||||
|
But make allowance for their doubting too:
|
||||||
|
|
||||||
|
If you can query and not be tired by waiting,
|
||||||
|
Or building models, adjust for outliers,
|
||||||
|
Define ETL modules that are self-validating,
|
||||||
|
And require contracts from data suppliers;
|
||||||
|
|
||||||
|
|
||||||
|
If you can report - and not make KPIs your master;
|
||||||
|
If you can forecast - and not make compliance your aim,
|
||||||
|
If you can make the overnight batch faster
|
||||||
|
And ensure the end results are just the same:
|
||||||
|
|
||||||
|
If you can't bear two versions of truth spoken
|
||||||
|
Produced by silos to make a trap for fools,
|
||||||
|
Or watch systems you gave your life to, broken,
|
||||||
|
And stoop and build 'em up with off-the-shelf tools;
|
||||||
|
|
||||||
|
|
||||||
|
If you would make one lake with all your data
|
||||||
|
And risk it on one vendor's big bang plan,
|
||||||
|
And lose, and start again at invitation-to-tender
|
||||||
|
And salvage from the project, what you can:
|
||||||
|
|
||||||
|
If you can force your flat files and mainframe
|
||||||
|
To serve datamarts when key DBAs retire,
|
||||||
|
And so hold on when stakeholders proclaim
|
||||||
|
Why (oh why) is our Data Quality so dire?
|
||||||
|
|
||||||
|
|
||||||
|
If you can talk with architects & keep your virtue,
|
||||||
|
Or walk with CEOs - nor lose the accounting touch,
|
||||||
|
If neither UTF-8 nor PII can hurt you,
|
||||||
|
If all teams count with you, but none too much:
|
||||||
|
|
||||||
|
If you can keep Data Owners beholden
|
||||||
|
To uploads that are timely and accurate and whole,
|
||||||
|
Yours is the Earth and clean records (golden),
|
||||||
|
And - which is more - you'll have Data Control!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*If you can't keep your data when all about you are losing theirs - [Data Controller for SAS](https://datacontroller.io) can help. Capture, review and approval for every change, with a full audit trail, on SAS Viya, SAS 9 EBI and SASjs Server.*
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Source LinkedIn post:
|
||||||
|
|
||||||
|
If you can keep your data when all about you
|
||||||
|
are losing theirs, and blaming it on you...
|
||||||
|
|
||||||
|
#datagovernance #dataquality #masterdatamanagement
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Image prompt:
|
||||||
|
Classic book title-page cover on a dark slate (#314351) background with a faint vignette and dot grid. A centred cream paper card with a drop shadow, double rule and green spine hint holds the title page: "FIRST PUBLISHED 2022" in small caps, green ornament rules, "IF" in large bold serif, "An Ode to Data Control" in serif, "after Rudyard Kipling" in italic, and the closing couplet "And - which is more - you'll have Data Control!" above a green rule. 1200x627 landscape, composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||||
|
-->
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: 'Struggling with Legacy Data Ingestion?'
|
||||||
|
description: Filename conventions, network drives, ancient formats and 5am batch failures - legacy spreadsheet ingestion hurts everyone. With Data Controller for SAS®, those issues disappear.
|
||||||
|
date: '2026-08-24 09:00:00'
|
||||||
|
author: 'Allan Bowe'
|
||||||
|
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||||
|
tags:
|
||||||
|
- Announcements
|
||||||
|
previewImg: './legacy-data-ingestion.jpeg'
|
||||||
|
---
|
||||||
|
|
||||||
|
Do you struggle against legacy data ingestion processes?
|
||||||
|
|
||||||
|
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
|
||||||
|
|
||||||
|
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
|
||||||
|
|
||||||
|
Sucks, right?
|
||||||
|
|
||||||
|
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
|
||||||
|
|
||||||
|
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
|
||||||
|
|
||||||
|
With Data Controller for SAS®, all these issues... disappear.
|
||||||
|
|
||||||
|
Thanks to our OEM licence of [SheetJS](https://sheetjs.com), analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
|
||||||
|
|
||||||
|
And the data really can be **anywhere** in the workbook - in any shape, spread across any number of disparate sheets, ranges and cells. A range of [import options](https://docs.datacontroller.io/excel/) handles everything from clean tabular sheets to sprawling financial reports.
|
||||||
|
|
||||||
|
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
|
||||||
|
|
||||||
|
Not only that - you can load ANY database, SAS dataset, format, or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
|
||||||
|
|
||||||
|
There is also a [community edition](https://datacontroller.io/pricing/), free for unlimited users (restricted by rows).
|
||||||
|
|
||||||
|
If you'd like to excel by spending less time on Excel - [let's chat](https://datacontroller.io/contact/).
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Source LinkedIn post:
|
||||||
|
|
||||||
|
Do you struggle against legacy data ingestion processes?
|
||||||
|
|
||||||
|
Are your analysts saving spreadsheets with a particular filename, on a particular network drive, on a particular day, with a specific structure?
|
||||||
|
|
||||||
|
Perhaps your process necessitates an ancient format (.xls, Excel '95) or particular encoding.
|
||||||
|
|
||||||
|
Sucks, right?
|
||||||
|
|
||||||
|
It sucks also for the Platform Manager, who gets the email at 5am when the batch breaks due to something not-quite-right in said input file.
|
||||||
|
|
||||||
|
Or the Data Engineer, spending all day knocking out generic excel import routines. Or the sponsor, paying for a broken process, that should take seconds.
|
||||||
|
|
||||||
|
With Data Controller for SAS®, all these issues... disappear.
|
||||||
|
|
||||||
|
Thanks to our OEM licence of SheetJS, analysts can self-serve data uploads via the browser - no (insecure) network drive needed, nor thick client install.
|
||||||
|
|
||||||
|
The data can be anywhere in the workbook - in any shape, across any number of disparate sheets, ranges and cells - thanks to a range of import options. Columns in any order. Additional columns (not in the target table) are simply ignored. Invalid columns cause immediate rejection, so the uploader can rectify.
|
||||||
|
|
||||||
|
Once automatic validations and DQ checks pass, and approval received, the target table is updated and any onward jobs are triggered. Needless to say, there is a full audit trail right back to the original excel file.
|
||||||
|
|
||||||
|
Not only that - you can load ANY database, #SAS dataset or CAS table. New data targets are configured by the admin - zero code, nothing to deploy or drag through to production.
|
||||||
|
|
||||||
|
There is also a community edition, free for unlimited users (restricted by rows).
|
||||||
|
|
||||||
|
If you'd like to excel by spending less time on Excel - let's chat.
|
||||||
|
|
||||||
|
#datamanagement #excel #datagovernance
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Image prompt:
|
||||||
|
A flat-design illustration in the Data Controller brand style (dark navy background, teal/green and orange accents), composed around a SINGLE CENTRAL focal point so it survives a square crop: in the centre, a large glowing browser window with a spreadsheet file being dropped into it and a green tick, feeding a short downward flow into a database cylinder directly beneath. Sinking into shadow at the very bottom of the frame, small and half-buried, the legacy relics being left behind: a floppy disk labelled .xls, a dusty network drive wrapped in chains, and an alarm clock showing 5am. Light radiates from the central browser window against the dark background. All key elements within the central square of the frame; outer left and right thirds contain only background texture and glow, safe to crop. Minimal text, no logos. Professional but lightly humorous, suitable for a B2B data product site. 16:9 landscape, 1200x627, suitable as a blog/feed cover image.
|
||||||
|
-->
|
||||||
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 754 KiB |
@@ -6,7 +6,7 @@ author: 'Data Controller'
|
|||||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||||
tags:
|
tags:
|
||||||
- Announcements
|
- Announcements
|
||||||
previewImg: './rollback-trains-meme.jpeg'
|
previewImg: './ctrl-z.jpeg'
|
||||||
---
|
---
|
||||||
|
|
||||||
# Oops! Now You Can Roll Back Data Changes
|
# Oops! Now You Can Roll Back Data Changes
|
||||||
@@ -15,6 +15,8 @@ Despite the MANY checks and guarantees in Data Controller for SAS®, sometimes i
|
|||||||
|
|
||||||
Thankfully, it is now possible to **roll back** data changes to a previous state.
|
Thankfully, it is now possible to **roll back** data changes to a previous state.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## How it works in practice
|
## How it works in practice
|
||||||
|
|
||||||
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
|
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
|
||||||
@@ -46,16 +48,18 @@ Because the rollback creates a new, approved changeset rather than silently rewi
|
|||||||
|
|
||||||
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
|
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
|
||||||
|
|
||||||
Full documentation is here: https://docs.datacontroller.io/rollback-data-changes/
|
Full documentation is here: https://docs.datacontroller.io/restore/
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Source LinkedIn post:
|
Source LinkedIn post:
|
||||||
|
|
||||||
"Which submission introduced this value?" should never be a hard question to answer.
|
|
||||||
|
|
||||||
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that - an uncomfortable audit conversation, a forensic exercise, and a quiet hope that nobody upstream consumed the bad data.
|
|
||||||
|
|
||||||
Data Controller now lets you ROLL BACK a table to a previous state.
|
"Can we put it back the way it was?" should never be a hard question to answer.
|
||||||
|
|
||||||
|
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that — a forensic exercise, an uncomfortable audit conversation, and a quiet hope that nobody upstream consumed the bad data.
|
||||||
|
|
||||||
|
Data Controller now lets you ROLL BACK a table to any previous state.
|
||||||
|
|
||||||
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
|
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
title: '28 Ways to Be Missing in SAS'
|
||||||
|
description: A SAS numeric missing is not a lone wolf - there are 28 of them, and Data Controller has supported all of them since v4. How they work, and what the validation rules do with them.
|
||||||
|
date: '2026-09-22 09:00:00'
|
||||||
|
author: 'Allan Bowe'
|
||||||
|
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||||
|
tags:
|
||||||
|
- Special Missings
|
||||||
|
- Data Quality
|
||||||
|
previewImg: './cover.jpeg'
|
||||||
|
---
|
||||||
|
|
||||||
|
# 28 Ways to Be Missing in SAS
|
||||||
|
|
||||||
|
A SAS numeric missing is not a lone wolf: the ordinary missing (`.`) is one of **28** distinct missing values a numeric variable can hold. The other 27 are written with a single character - the letters `A` to `Z`, or an underscore (`._`).
|
||||||
|
|
||||||
|
They exist because "missing" is usually not the whole story. A survey question that was never reached, a reading that was illegible, a value the respondent refused to give - in a well-run process those are different facts, and a lone `.` throws the difference away. Special missings record *why* the value is missing.
|
||||||
|
|
||||||
|
## They are numbers, not text
|
||||||
|
|
||||||
|
- Arithmetic on them yields missing - `.A + 1` is `.`
|
||||||
|
- `PROC MEANS`, `PROC SUMMARY` and the other summarisation procedures exclude them, exactly as they exclude `.`
|
||||||
|
- They sort below every non-missing number, in the order `._`, then `.`, then `.A` to `.Z`
|
||||||
|
- `NMISS()` and `CMISS()` count them as missing
|
||||||
|
- Converting one to text drops the period - `CATS(.A)` is the string `A`
|
||||||
|
|
||||||
|
One display detail is worth knowing: a regular missing prints as `.`, unless `OPTIONS MISSING=` changes that character - set it to blank and a regular missing renders as an empty cell. The option affects only the regular missing; `._` and `.A`-`.Z` always print as their own letter. So a blank cell in a SAS listing is still unambiguously a regular missing, and a lone letter is still a special one. Data Controller itself is unaffected either way - it sends a regular missing to the browser as `null` and a special missing as its letter.
|
||||||
|
|
||||||
|
In a SAS dataset they are written with a leading period (`.A`, `.B` ... `._`). In Data Controller you type the letter or the underscore, with or without that period - `.a` and `a` are the same missing - and the letter is not case sensitive. Two letters, or a letter mixed with a number, are refused rather than guessed.
|
||||||
|
|
||||||
|
There is one cell where the letter cannot be typed at all. A numeric column that carries a date, datetime or time format is edited through a date picker rather than the numeric editor, and a picker accepts only a date.
|
||||||
|
|
||||||
|
## Carrying them between the browser and SAS
|
||||||
|
|
||||||
|
The Data Controller frontend and the SAS backend exchange data as JSON, and JSON has no way to express a letter as a numeric value - `A` is a string. The conversion is handled in the open source [SASjs Adapter](https://github.com/sasjs/adapter#variable-types):
|
||||||
|
|
||||||
|
- The adapter infers each column's SAS type from the values it is given. All numeric values mean a numeric column, all strings mean a character column, and a column holding a single character (`a`-`z`, `_` or `.`) alongside numeric values is numeric, with the lone characters written to SAS as special missings.
|
||||||
|
- `null` becomes `.` or an empty string, according to the type derived for that column.
|
||||||
|
- Two cases cannot be inferred from the values alone: a numeric column containing *only* special missings looks like a single character column, and a character column containing only nulls looks numeric. For those, the adapter accepts an explicit format for the column - and Data Controller sends one automatically, because it already knows each column's SAS format from the metadata returned by the backend.
|
||||||
|
- A value that is neither a number nor a single valid character is refused rather than guessed - `aaaa`, or `!` in a numeric column.
|
||||||
|
- A lone `.` is accepted as another way of typing the regular missing.
|
||||||
|
|
||||||
|
There is nothing to configure. Special missings are available by default, for numeric cells - a date, datetime or time formatted column aside, since those edit through a date picker.
|
||||||
|
|
||||||
|
Once in SAS they are ordinary values, so they are what the approval DIFF screen compares, and the DIFF's formatted / unformatted switch shows either the formatted representation or the raw value - useful for confirming exactly which missing was set.
|
||||||
|
|
||||||
|
## What the Data Controller validation rules do with them
|
||||||
|
|
||||||
|
These are Data Controller's own rules, configured per column in the `MPE_VALIDATIONS` table and applied in the browser as you edit and submit.
|
||||||
|
|
||||||
|
- `NOTNULL` - rejects one. A special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it as well. A primary key column is treated as NOT NULL whether or not a rule is configured for it
|
||||||
|
- `HARDREGEX` - checked against the pattern like any other value; unlike blanks and the plain `.`, special missings are **not** exempt, so a numeric column that carries them needs a pattern which allows for a single letter
|
||||||
|
- `SOFTREGEX` - the same check, but a failure is only a warning rather than a block, and it is ignored entirely if the column also has a `HARDREGEX` rule
|
||||||
|
- `SOFTSELECT` / `HARDSELECT` - both support them. The dropdown lists a special missing as a bare letter alongside the ordinary values, and a hard rule then accepts it like any other listed value - it still rejects a value that is not in the list
|
||||||
|
- `ROUND` - no effect. It only rounds values that are numbers, so a special missing is left as it was typed
|
||||||
|
|
||||||
|
The range rules compare in the order SAS itself uses, which is what lets a range be written in special missings. SAS puts every missing below every non-missing value, and orders the missing values among themselves: `._` is the lowest, then the regular missing, then `.A` through `.Z`. A range therefore means exactly what SAS would mean by it:
|
||||||
|
|
||||||
|
- `MINVAL .A` with `MAXVAL .C` accepts `.B` and rejects `.D`, and a blank - the regular missing - fails that floor because it sorts below `.A`
|
||||||
|
- a number sits above every missing, so it passes a floor of `.A` and fails a ceiling of `.C`
|
||||||
|
- against a numeric bound the ordering does the obvious thing: a missing sorts below every number, so it fails a `MINVAL` of 1 and passes a `MAXVAL` of 100. Use `NOTNULL` if the column has to be populated
|
||||||
|
- a rule value that is neither a number nor a special missing - a typo such as `..` or `AB` - satisfies nothing, so the column fails until the rule is corrected
|
||||||
|
|
||||||
|
A formula is the one case where a special missing genuinely does not work:
|
||||||
|
|
||||||
|
- `HARDFORMULA` / `SOFTFORMULA` - a formula that reads a special-missing cell does not compute; the grid's spreadsheet engine returns `#VALUE!` for that row, where the plain `.` contributes 0
|
||||||
|
|
||||||
|
`CASE` (`UPCASE` / `LOWCASE`) is a character rule, so it does not apply: SAS hands the browser a special missing as an uppercase letter, so there is no case to enforce, and a `CASE` rule on a numeric column would reject the column's numbers rather than the missing.
|
||||||
|
|
||||||
|
Worth knowing about the regex rules: although the pattern is written in SAS PRX syntax, the check itself runs in the browser - SAS only parses the pattern (`PRXPARSE`) when the rule is saved - so the two engines can disagree on exotic patterns, and SAS pads a numeric-to-character conversion, so a pattern re-used in SAS needs `strip()` for an anchored match.
|
||||||
|
|
||||||
|
In short, a special missing counts as a value for the pattern and dropdown rules, and takes its own place in the order for the range rules: it sits below every number, so a numeric minimum rejects it and a numeric maximum accepts it, while a range written in special missings is decided among the missing values themselves.
|
||||||
|
|
||||||
|
## See it in action
|
||||||
|
|
||||||
|
The recording below runs the whole cycle on one table: entering special missings, the rules that reject them, submitting the changes, approving them, and reviewing the DIFF - including a change from one special missing to another, and the formatted / unformatted switch on a date column.
|
||||||
|
|
||||||
|
It also shows the range rules doing what the section above describes. A special missing is refused by a numeric `MINVAL` and accepted by a numeric `MAXVAL`, and on a column carrying `MINVAL .A` with `MAXVAL .C`, `.B` is taken while `.D` is refused.
|
||||||
|
|
||||||
|
<div style="position: relative; padding-top: 56.25%; margin-bottom: 2rem;"><iframe title="Special Missings in Data Controller" width="100%" height="100%" src="https://vid.4gl.io/videos/embed/9UQZzCNBU3zPNQYxdzyV3A?peertubeLink=0" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
LinkedIn version of this post - publish it with the "Special Missings in Data
|
||||||
|
Controller" video attached. Kept in sync with the copy above: same points, same
|
||||||
|
claims, same order.
|
||||||
|
|
||||||
|
A SAS numeric missing is not a lone wolf. There are 28 of them.
|
||||||
|
|
||||||
|
The ordinary missing (.) is the best known of the 28. The other 27 are single characters - the letters A to Z, or an underscore (._) - and they let you record WHY a value is missing: the question was never reached, the reading was illegible, the respondent refused.
|
||||||
|
|
||||||
|
They are real numeric values, not text:
|
||||||
|
|
||||||
|
- .A + 1 is .
|
||||||
|
- PROC MEANS excludes them, like any other missing
|
||||||
|
- they sort below every number: ._ then . then .A to .Z
|
||||||
|
- NMISS() and CMISS() count them as missing
|
||||||
|
- a regular missing prints as . unless OPTIONS MISSING= changes it - and that option never touches a special missing
|
||||||
|
|
||||||
|
The hard part is every tool that is not SAS. JSON has no way to say "this number is a letter" - A is just a string. So a value that is perfectly legal in a SAS dataset quietly breaks in the browser, in Excel, in an API.
|
||||||
|
|
||||||
|
We solved that in the open source SASjs Adapter, and it has been in Data Controller for SAS since v4:
|
||||||
|
|
||||||
|
- the adapter infers the column type from the values, so a column of numbers that also holds a lone letter is written to SAS as numeric, with the letter as a special missing
|
||||||
|
- where the type cannot be inferred - a numeric column holding ONLY special missings - Data Controller passes the column format explicitly
|
||||||
|
- you type the letter or the underscore, with or without the period (a, .a, _, ._), and case does not matter
|
||||||
|
- one exception: a date, datetime or time formatted numeric column edits through a date picker, which takes only a date
|
||||||
|
|
||||||
|
How they behave in Data Controller's validation rules:
|
||||||
|
|
||||||
|
- NOTNULL rejects one: a special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it too. A primary key column is NOT NULL whether or not a rule is configured
|
||||||
|
- HARDREGEX checks it against the pattern, unlike blanks and plain .
|
||||||
|
- SOFTREGEX warns instead of blocking (and is ignored if the column also has a HARDREGEX)
|
||||||
|
- SOFTSELECT and HARDSELECT both support them - the dropdown lists the missing as a bare letter, and a hard rule accepts it like any other listed value
|
||||||
|
- ROUND leaves it alone, because it only rounds numbers
|
||||||
|
- a HARDFORMULA/SOFTFORMULA that reads a special-missing cell returns #VALUE! rather than a number
|
||||||
|
|
||||||
|
The range rules compare in SAS's own order, so a range can be written in special missings: MINVAL .A with MAXVAL .C accepts .B and rejects .D. A missing sorts below every number, so it fails a numeric MINVAL and passes a numeric MAXVAL; a number sits above every missing.
|
||||||
|
|
||||||
|
In short: a value for the pattern and dropdown rules, and its own place in the order for the range rules - below every number, so a numeric minimum rejects it and a numeric maximum accepts it.
|
||||||
|
|
||||||
|
The video shows the whole cycle - entering them, the rejections, submit, approve, and the DIFF, including a range written in special missings.
|
||||||
|
|
||||||
|
#sas #datacapture #mdm #dataquality
|
||||||
|
|
||||||
|
video: https://vid.4gl.io/w/9UQZzCNBU3zPNQYxdzyV3A
|
||||||
|
|
||||||
|
Image prompt: Cinematic editorial cover illustration: a large pack of wolves, a
|
||||||
|
dozen or more, spread wide and moving together across a vast snow plain at dusk,
|
||||||
|
seen from a low wide angle. One wolf stands apart from the group on the left of
|
||||||
|
frame, turned back toward the pack. The pack is rendered in cool blue-grey and
|
||||||
|
silver; the lone wolf catches the only warm light in the scene, a single low
|
||||||
|
amber sun. Overcast dusk sky, faint falling snow, long soft shadows, generous
|
||||||
|
empty sky and snow to the upper third so the wide crop breathes. Painterly
|
||||||
|
digital illustration, muted desaturated palette, soft depth of field with the
|
||||||
|
distant wolves falling out of focus, no text, no letters, no numbers, no logos,
|
||||||
|
no watermark.
|
||||||
|
|
||||||
|
Constraints: no text, no letters, no numbers, no digits, no symbols, no
|
||||||
|
captions, no logos, no watermark, no signature, no border, no frame, no collar,
|
||||||
|
no harness, no humans, no buildings, and do not ask for an exact head count of
|
||||||
|
28 - a crowded pack reads worse than a dozen clear animals, and the number
|
||||||
|
belongs in the headline, not the artwork. The no-text rule matters more than
|
||||||
|
usual here: the subject is letters standing in for numbers, so a stray glyph
|
||||||
|
anywhere undercuts the cover.
|
||||||
|
|
||||||
|
Output: save as ./cover.jpeg at 1.91:1 (1200x627, matching the other feed
|
||||||
|
covers and doubling as the LinkedIn share card), and set
|
||||||
|
previewImg: './cover.jpeg' in the front matter above. Do not also embed the
|
||||||
|
image in the markdown body. Full prompt, variants and rationale:
|
||||||
|
https://paste.4gl.io/?513711f5bde3610e#9LZCA9xzQHDy3CycXmsWU5AoVNtjBZmPotUHHtHTAerF
|
||||||
|
|
||||||
|
Fallback variant if the pack composition comes back muddled: a single wolf
|
||||||
|
standing alone on a snow plain at dusk, its shadow stretching toward a distant
|
||||||
|
pack reduced to small silhouettes on the horizon.
|
||||||
|
-->
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { FaShieldAlt } from 'react-icons/fa'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const StyledCertBadge = styled.a`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 2px solid #314351;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #314351;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: #314351;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const CertBadge = () => (
|
||||||
|
<StyledCertBadge
|
||||||
|
href="https://sasapps.io/cyber-essentials-certified/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
<FaShieldAlt /> Cyber Essentials Certified
|
||||||
|
</StyledCertBadge>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default CertBadge
|
||||||
@@ -5,6 +5,7 @@ import Layout from '../components/layout'
|
|||||||
import Seo from '../components/seo'
|
import Seo from '../components/seo'
|
||||||
|
|
||||||
import { Section } from '../components/shared'
|
import { Section } from '../components/shared'
|
||||||
|
import CertBadge from '../components/shared/certBadge'
|
||||||
import {
|
import {
|
||||||
SectionHeading,
|
SectionHeading,
|
||||||
SectionDesc
|
SectionDesc
|
||||||
@@ -37,6 +38,7 @@ const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
|||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</SectionDesc>
|
</SectionDesc>
|
||||||
|
<CertBadge />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Layout from '../components/layout'
|
|||||||
import Seo from '../components/seo'
|
import Seo from '../components/seo'
|
||||||
|
|
||||||
import { Section, ScheduleDemo } from '../components/shared'
|
import { Section, ScheduleDemo } from '../components/shared'
|
||||||
|
import CertBadge from '../components/shared/certBadge'
|
||||||
import {
|
import {
|
||||||
SectionHeading,
|
SectionHeading,
|
||||||
SectionDesc
|
SectionDesc
|
||||||
@@ -98,6 +99,7 @@ const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
|||||||
perform manual data uploads into their preferred database, in
|
perform manual data uploads into their preferred database, in
|
||||||
real-time, with full validation, approval, security, and control.
|
real-time, with full validation, approval, security, and control.
|
||||||
</SectionDesc>
|
</SectionDesc>
|
||||||
|
<CertBadge />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-3">
|
<div className="col-md-3">
|
||||||
<Art src={rightArt} info="Clinical Research Data" />
|
<Art src={rightArt} info="Clinical Research Data" />
|
||||||
|
|||||||
@@ -118,6 +118,22 @@ const StyledContent = styled.div`
|
|||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
font-family: Monaco, 'Andale Mono', 'Courier New', Courier, monospace;
|
font-family: Monaco, 'Andale Mono', 'Courier New', Courier, monospace;
|
||||||
}
|
}
|
||||||
|
/* A fenced block tagged with the 'poem' language is verse, not code: drop
|
||||||
|
the code chrome and set it in the serif face, keeping the author's line
|
||||||
|
breaks and blank lines (which read as stanza breaks). */
|
||||||
|
pre:has(> code.language-poem) {
|
||||||
|
background-image: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1.8rem 0;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
pre > code.language-poem {
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #222222;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
interface PostProps {
|
interface PostProps {
|
||||||
|
|||||||