Compare commits
42
Commits
v7.11.0
...
regex-info
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d13fab267f | ||
|
|
44bc7f7fea | ||
|
|
f60bcef583 | ||
|
|
e7abb0a08a | ||
|
|
cfb60e5e4b | ||
|
|
33dcb989d3 | ||
|
|
aaf406b386 | ||
|
|
8efea8c744 | ||
|
|
39c8855f37 | ||
|
|
5c56c7579f | ||
|
|
92c1e20126 | ||
|
|
7050808d80 | ||
|
|
37a98e1d64 | ||
|
|
66f7b87b07 | ||
|
|
fae9496bbc | ||
|
|
cfe1e75be4 | ||
|
|
b51c770782 | ||
|
|
2a771bb91a | ||
|
|
54b8c78f86 | ||
|
|
ea74f95144 | ||
|
|
bd798b424a | ||
|
|
ee3c6c9e0f | ||
|
|
a3e46a968e | ||
|
|
ee4e9b9271 | ||
|
|
7a35cf4a45 | ||
|
|
d881290618 | ||
|
|
994e7fc973 | ||
|
|
578c403994 | ||
|
|
56b7854db5 | ||
|
|
7ed3730ae3 | ||
|
|
6cd9b68581 | ||
|
|
7378f3ba30 | ||
|
|
cac9244f92 | ||
|
|
ea00c5afad | ||
|
|
359d833406 | ||
|
|
022981390e | ||
|
|
05fe4744d5 | ||
|
|
e22edf7ed3 | ||
|
|
62ff0aee4a | ||
|
|
3ed7cfdbee | ||
|
|
d2c93a46fa | ||
|
|
17e4802895 |
@@ -0,0 +1,19 @@
|
||||
# Dependency Updates Checklist
|
||||
|
||||
Whenever any `package.json` (root, `client/`, or `sas/`) or lockfile is modified, run the same checks CI runs before pushing:
|
||||
|
||||
1. **npm audit** (must be clean for prod deps):
|
||||
```bash
|
||||
npm audit --omit=dev # in repo root
|
||||
cd sas && npm audit --omit=dev
|
||||
cd ../client && npm audit --omit=dev
|
||||
```
|
||||
Fix with `npm audit fix`, targeted `overrides` in `package.json`, or version bumps — never `npm audit fix --force` blindly, as it can introduce breaking changes.
|
||||
|
||||
2. **License checker** (client only):
|
||||
```bash
|
||||
cd client && npm run license-checker
|
||||
```
|
||||
If a new dependency fails, either add its SPDX id to the `onlyAllow` list in `client/licenseChecker.js` (if the license is acceptable, e.g. permissive ones like `BlueOak-1.0.0`) or add the specific package to `excludePackages` with justification. Data Controller ships on-prem, so only OSI-approved permissive licenses are acceptable for production dependencies.
|
||||
|
||||
Both checks run in `.gitea/workflows/build.yaml` (`Check audit` and `Licence checker` steps) and will fail the build if skipped locally.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Row Level Security — Technical Deep Dive
|
||||
|
||||
This document explains **how** Row Level Security (RLS) is implemented in the Data Controller backend. For the user-facing configuration guide, see [docs.datacontroller.io/row-level-security](https://docs.datacontroller.io/row-level-security/).
|
||||
|
||||
## Overview
|
||||
|
||||
RLS in Data Controller is implemented as **server-side WHERE clause generation**. No data leaves SAS without passing through a dynamically generated filter. The filter is built at runtime per request, based on:
|
||||
|
||||
1. The requesting user's group memberships (SAS metadata groups + DC groups)
|
||||
2. The active rules in the `MPE_ROW_LEVEL_SECURITY` control table
|
||||
3. The access mode (VIEW / EDIT / download / upload)
|
||||
|
||||
Because the filter is expressed as a standard SAS `WHERE` expression, it works against **any** engine — Base SAS datasets, database libraries (via implicit SQL pass-through pushdown), SPDE, CAS libnames, etc.
|
||||
|
||||
## Request Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Client request\nview / edit / download / upload] --> B{Service mode}
|
||||
B -->|VIEW / EDIT / DLOAD| C["%mpe_filtermaster(mode, libds)"]
|
||||
B -->|ULOAD stagedata.sas| C
|
||||
C --> D["%mpe_getgroups()\nmetadata groups + MPE_GROUPS"]
|
||||
D --> E{User in &mpeadmins?}
|
||||
E -->|Yes| F[No RLS filter\n1=1]
|
||||
E -->|No| G[Lookup active rules in\nMPE_ROW_LEVEL_SECURITY\nfor libref.table + user's groups]
|
||||
G --> H{Rules found?}
|
||||
H -->|No| F
|
||||
H -->|Yes| I["%mp_filtergenerate()\nper group, OR'd together"]
|
||||
I --> J[WHERE clause written\nto temp fileref]
|
||||
F --> J
|
||||
J --> K{Read or Write?}
|
||||
K -->|Read\nviewdata / getdata / getrawdata| L["where %inc filtref;;\nrows filtered server-side"]
|
||||
K -->|Write\nstagedata| M[Inverse filter\nwhere not( filtref )]
|
||||
M --> N{badrecords > 0?}
|
||||
N -->|Yes| O[Abort submission\nSecurity Problem]
|
||||
N -->|No| P[Staging proceeds\nto approval workflow]
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
| Component | Location | Role |
|
||||
|---|---|---|
|
||||
| `MPE_ROW_LEVEL_SECURITY` | `&mpelib` (DC control library) | The rule table (scope, group, libref, table, logic, subgroup, variable, operator, raw value, active flag) |
|
||||
| `%mpe_filtermaster()` | `sas/sasjs/macros/mpe_filtermaster.sas` | Master macro that assembles the full WHERE clause for a request |
|
||||
| `%mpe_getgroups()` | `sas/sasjs/macros/mpe_getgroups.sas` | Resolves group membership (metadata groups via `%dc_getusergroups` + `MPE_GROUPS` DC-internal groups) |
|
||||
| `%mp_filtergenerate()` | SASjs core | Converts a query table (logic/subgroup/variable/operator/value rows) into WHERE clause text |
|
||||
| `%mp_filtercheck()` | SASjs core | Validates rule syntax at *edit time* (defence against SAS code injection) |
|
||||
| `mpe_row_level_security_postedit.sas` | `sas/sasjs/services/hooks/` | Post-edit hook that runs `%mp_filtercheck` whenever the RLS table itself is edited |
|
||||
|
||||
## The Modes
|
||||
|
||||
`%mpe_filtermaster` accepts a `mode` parameter, and every service that surfaces data calls it with the appropriate mode:
|
||||
|
||||
| Mode | Caller | Purpose |
|
||||
|---|---|---|
|
||||
| `VIEW` | `services/public/viewdata.sas`, `getchangeinfo.sas` | Read-only table viewer |
|
||||
| `EDIT` | `services/editors/getdata.sas` | The EDIT grid (adds "current records only" validity filtering) |
|
||||
| `DLOAD` | `services/public/getrawdata.sas` | Raw file downloads (RLS scope treated as `VIEW`) |
|
||||
| `ULOAD` | `services/editors/stagedata.sas` | **Upload validation** (RLS scope treated as `EDIT`) |
|
||||
|
||||
Scope mapping: `DLOAD` requests match rules with `RLS_SCOPE in ('VIEW','ALL')`; `ULOAD` requests match rules with `RLS_SCOPE in ('EDIT','ALL')`.
|
||||
|
||||
## Execution Flow of `%mpe_filtermaster`
|
||||
|
||||
The macro writes the final WHERE expression to a temporary **fileref** (`outref`), line by line. (A fileref is used because a generated filter may exceed the 64k macro variable limit — and note that `%include` of a fileref is not allowed directly in a `proc sql` where clause, hence callers typically use it in a DATA step or data step view.)
|
||||
|
||||
### 1. User-supplied filter (FILTER_RK)
|
||||
|
||||
If the request includes a stored filter (`filter_rk > 0`), its clauses are read from `MPE_FILTERANYTABLE` / `MPE_FILTERSOURCE` and generated first via `%mp_filtergenerate`. RLS clauses are then **AND-ed on top** — a user filter can only ever narrow results, never widen them beyond RLS.
|
||||
|
||||
### 2. Validity-date filtering (EDIT / DLOAD only)
|
||||
|
||||
`MPE_TABLES` may define `VAR_TXFROM` / `VAR_TXTO` (SCD2-style validity variables) for the target table. Unless the user explicitly filtered on those variables, the macro appends:
|
||||
|
||||
```
|
||||
("<current datetime>"dt < VAR_TXTO)
|
||||
```
|
||||
|
||||
so that only current records are surfaced in the EDIT grid and downloads.
|
||||
|
||||
### 3. Group resolution and admin bypass
|
||||
|
||||
```sas
|
||||
%mpe_getgroups(user=%mf_getuser(), outds=work.groups)
|
||||
```
|
||||
|
||||
Groups come from two sources, concatenated:
|
||||
|
||||
* SAS metadata groups (Viya / EBI / Base-specific logic in `%dc_getusergroups`)
|
||||
* The `MPE_GROUPS` DC table (group assignments managed inside Data Controller)
|
||||
|
||||
If the user is a member of the `&mpeadmins` group, **RLS is skipped entirely** — admins always see all rows.
|
||||
|
||||
### 4. Rule extraction
|
||||
|
||||
Non-admin users trigger a lookup of active, current rules:
|
||||
|
||||
```sas
|
||||
create table work.&rlsds as
|
||||
select rls_group, rls_group_logic, rls_subgroup_logic, rls_subgroup_id,
|
||||
rls_variable_nm, rls_operator_nm, rls_raw_value
|
||||
from &mpelib..mpe_row_level_security
|
||||
where &dc_dttmtfmt. lt tx_to /* only current (non-deleted) rules */
|
||||
and rls_scope in ("&scopeval",'ALL')
|
||||
and upcase(rls_group) in (select upcase(groupname) from work.groups)
|
||||
and rls_libref = "<libref>" and rls_table = "<dsname>"
|
||||
and rls_active = 1
|
||||
order by rls_group, rls_subgroup_id;
|
||||
```
|
||||
|
||||
### 5. Clause assembly
|
||||
|
||||
If rules exist, they are appended to the fileref as `AND ( ... )`. Each **group** the user belongs to contributes one sub-filter, and the group-level sub-filters are joined with `OR`:
|
||||
|
||||
```
|
||||
AND ( <group 1 filter> OR <group 2 filter> ... )
|
||||
```
|
||||
|
||||
Each group's filter is produced by `%mp_filtergenerate`, which honours:
|
||||
|
||||
* `RLS_GROUP_LOGIC` — how subgroups (identified by `RLS_SUBGROUP_ID`) are joined
|
||||
* `RLS_SUBGROUP_LOGIC` — how individual clauses within a subgroup are joined (AND/OR)
|
||||
|
||||
So membership in multiple groups is **permissive** (OR): the user sees the union of rows permitted by each of their groups.
|
||||
|
||||
### 6. Empty-filter fallback
|
||||
|
||||
If nothing was written to the fileref (no stored filter, no validity vars, no matching RLS rules), the macro writes a literal `1=1` so the fileref can always be consumed as a valid WHERE expression. Absence of rules therefore means **no restriction** — RLS is opt-in per table/group.
|
||||
|
||||
## How Callers Apply the Filter
|
||||
|
||||
### Read path (viewdata.sas)
|
||||
|
||||
```sas
|
||||
%mpe_filtermaster(VIEW, &libds, dclib=&mpelib, filter_rk=&filter_rk,
|
||||
outref=filtref, outds=work.query)
|
||||
|
||||
data work.viewdata;
|
||||
set &libds;
|
||||
where %inc filtref;;
|
||||
if _n_ > &DC_MAXOBS_WEBVIEW then stop;
|
||||
run;
|
||||
```
|
||||
|
||||
The fileref is `%include`d directly inside the `where` statement — the filter never passes through client-visible state, so it cannot be tampered with. For database libraries the WHERE expression is pushed down to the database by the SAS engine.
|
||||
|
||||
### Write path (stagedata.sas) — inverse filter
|
||||
|
||||
Uploads cannot be filtered; instead the filter is **inverted** and any submitted row matching the inverse is rejected:
|
||||
|
||||
```sas
|
||||
%mpe_filtermaster(ULOAD, &libds, dclib=&mpelib, outref=filtref, ...)
|
||||
|
||||
/* prepare inverse query */
|
||||
data _null_;
|
||||
infile filtref end=eof;
|
||||
file &tempref;
|
||||
if _n_=1 then put 'where not(';
|
||||
input; put _infile_;
|
||||
if eof then put ')';
|
||||
run;
|
||||
|
||||
data work.badrecords;
|
||||
set work.jsdata; /* rows submitted by the user */
|
||||
%inc &tempref/source2;;
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(%mf_nobs(work.badrecords)>0)
|
||||
,msg=%str(Security Problem - N unauthorised records submitted))
|
||||
```
|
||||
|
||||
If even one submitted row falls outside the user's permitted row set, the entire staging request is aborted before any approval/apply step.
|
||||
|
||||
## Incompatibility with REPLACE Load Type
|
||||
|
||||
RLS with `EDIT` scope is **incompatible** with tables configured with `LOAD_TYPE=REPLACE` in `MPE_TABLES`. A REPLACE load wipes and reloads the entire target table, so row-level filtering of submitted records cannot be enforced meaningfully (the rows a user is *not* allowed to see would also be deleted). Backend validations therefore abort in both directions (see [issue #211](https://git.datacontroller.io/dc/dc/issues/211)):
|
||||
|
||||
1. **`mpe_row_level_security_postedit.sas`** — aborts when activating a rule with `RLS_SCOPE in ('EDIT','ALL')` against a table whose current `MPE_TABLES` record has `LOADTYPE='REPLACE'`.
|
||||
2. **`mpe_tables_postedit.sas`** — aborts when setting `LOADTYPE='REPLACE'` on a table that already has active, current `RLS_SCOPE in ('EDIT','ALL')` rules in `MPE_ROW_LEVEL_SECURITY`.
|
||||
|
||||
`VIEW`-scope rules remain compatible with REPLACE loads, since they only affect read paths.
|
||||
|
||||
## Edit-Time Validation (Injection Defence)
|
||||
|
||||
Because `RLS_RAW_VALUE` is free text that ends up inside a generated WHERE clause, it is a potential SAS code-injection vector. Mitigations:
|
||||
|
||||
1. **`mpe_row_level_security_postedit.sas`** — a post-edit hook on the `MPE_ROW_LEVEL_SECURITY` table itself. Every newly staged rule (with `rls_active=1`) is grouped by target `libref.table` and run through `%mp_filtercheck(targetds=..., abort=YES)`, which compiles/tests each clause against the real target table and rejects the whole submission on invalid syntax (see [mp_filtercheck](https://core.sasjs.io/mp__filtercheck_8sas.html)).
|
||||
2. **Format rules** enforced by validation: character values must be single quoted, `IN`/`NOT IN` values must be bracketed, `BETWEEN` must contain `AND`.
|
||||
|
||||
Additionally, editing `MPE_ROW_LEVEL_SECURITY` is itself a Data Controller table edit, so it goes through the normal approval workflow, audit trail (`MPE_AUDIT`), and can be column-restricted via [Column Level Security](https://docs.datacontroller.io/column-level-security/).
|
||||
|
||||
## Worked Example
|
||||
|
||||
Given these active rules (all for `MYLIB.MYDS`, `RLS_ACTIVE=1`):
|
||||
|
||||
| SCOPE | GROUP | GROUP_LOGIC | SUBGRP_LOGIC | SUBGRP_ID | VAR | OP | VALUE |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| ALL | Group 1 | AND | AND | 1 | VAR_2 | IN | ('this','or') |
|
||||
| ALL | Group 1 | AND | AND | 1 | VAR_3 | < | 42 |
|
||||
| ALL | Group 2 | AND | AND | 1 | VAR_4 | CONTAINS | 'xyz' |
|
||||
|
||||
A non-admin user in **both** groups, opening the VIEW page, gets a fileref containing:
|
||||
|
||||
```
|
||||
AND ( (VAR_2 IN ('this','or') AND VAR_3 < 42) OR (VAR_4 CONTAINS 'xyz') )
|
||||
```
|
||||
|
||||
resulting in:
|
||||
|
||||
```sas
|
||||
data work.viewdata;
|
||||
set mylib.myds;
|
||||
where (VAR_2 IN ('this','or') AND VAR_3 < 42) OR (VAR_4 CONTAINS 'xyz');
|
||||
run;
|
||||
```
|
||||
|
||||
The same user submitting an EDIT upload has the inverse applied to their staged rows; any row not matching the expression above aborts the submission.
|
||||
|
||||
## Summary of Security Properties
|
||||
|
||||
* **Enforced server-side** in every read service (VIEW/EDIT/DLOAD) and every write service (ULOAD via inverse filter).
|
||||
* **Admin bypass** is explicit (`&mpeadmins` group membership check).
|
||||
* **Multi-group semantics are OR** (union of permitted rows); rules within a group/subgroup are AND/OR per configuration.
|
||||
* **Fail-open by design**: if no rules match a table, it is unrestricted — RLS must be opted into per table.
|
||||
* **Injection-resistant**: values are validated with `%mp_filtercheck` at the time rules are edited, not at query time.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Testing
|
||||
|
||||
Backend (SAS) tests are run with the sasjs CLI from the `sas/` directory.
|
||||
|
||||
## Commands
|
||||
|
||||
- Full deploy + test cycle: `npm run 4gl` (compiles, deploys to the 4gl target, runs makedata) then `sasjs test -t 4gl`.
|
||||
- Run tests only (no rebuild/redeploy): `sasjs test -t 4gl`.
|
||||
- Run a subset of tests: `sasjs test -t 4gl SOMESTRING` — the positional argument filters to matching tests (e.g. `sasjs test -t 4gl stagedata.test.3`).
|
||||
|
||||
## Results
|
||||
|
||||
- Test results: `sas/sasjsresults/testResults.json` / `.csv` / `.xml`
|
||||
- Individual test logs: `sas/sasjsresults/logs/<testsuite>.log` (e.g. `services_editors_stagedata.test.3.log`)
|
||||
- Coverage: `sas/sasjsresults/coverage.lcov`
|
||||
|
||||
## Notes
|
||||
|
||||
- Test source files live in `sas/sasjs/**`; `sas/sasjsbuild/` is generated build output — do not hand-edit it.
|
||||
- `sasjs test` executes tests against the deployed app, so run `npm run 4gl` first after changing any service/hook/test code.
|
||||
- Assertions are made with `%mp_assert()`; results are written to `work.test_results`.
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
# Audit should fail and stop the CI on any vulnerability in root and sas, and on low+ in client
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
|
||||
- name: Lint check
|
||||
run: npm run lint:check
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
# Start frontend and run cypress
|
||||
# timeout 1800: SIGTERM after 30 min so Cypress can flush video/screenshots
|
||||
# before the outer timeout-minutes hard-kills the step (avoids silent multi-hour hangs)
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts"
|
||||
npx ng serve --host 0.0.0.0 --port 4200 & npx wait-on http://localhost:4200 && timeout 1800 npx cypress run --browser chrome --spec "cypress/e2e/csv-limited.cy.ts,cypress/e2e/liveness.cy.ts,cypress/e2e/editor.cy.ts,cypress/e2e/excel-multi-load.cy.ts,cypress/e2e/excel.cy.ts,cypress/e2e/csv.cy.ts,cypress/e2e/filtering.cy.ts,cypress/e2e/licensing.cy.ts,cypress/e2e/viewer-labels.cy.ts,cypress/e2e/viewbox.cy.ts"
|
||||
|
||||
- name: Zip Cypress videos
|
||||
if: always()
|
||||
|
||||
@@ -45,13 +45,13 @@ jobs:
|
||||
npm ci
|
||||
|
||||
- name: Check audit
|
||||
# Audit should fail and stop the CI if critical vulnerability found
|
||||
# Audit should fail and stop the CI on any vulnerability in root and sas, and on low+ in client
|
||||
run: |
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
cd ./sas
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
cd ../client
|
||||
npm audit --audit-level=critical --omit=dev
|
||||
npm audit --omit=dev
|
||||
|
||||
- name: Angular Tests
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Instructions
|
||||
|
||||
## Git
|
||||
|
||||
Do NOT auto-commit. Never run `git commit` (or `git push`) unless the user explicitly asks. Leave changes in the working tree for the user to review and commit themselves.
|
||||
|
||||
## Markdown files
|
||||
|
||||
Do NOT hard-wrap Markdown files (no fixed-width line wrapping / carriage returns inside paragraphs). Each paragraph, list item, and heading should be a single logical line, regardless of length. Let the editor/viewer soft-wrap. This includes AGENTS.md itself.
|
||||
|
||||
This is different from `.sas` files, where a maximum line length applies. The no-wrap rule applies to all `*.md` files in this repo (docs, READMEs, etc.).
|
||||
|
||||
Rationale: hard-wrapped prose produces noisy diffs when sentences are edited and reflowed, and Markdown renderers already handle wrapping.
|
||||
|
||||
## SAS files
|
||||
|
||||
After creating or modifying any `.sas` files, run `sasjs lint` from the `sas/` directory and ensure the files you touched have no lint warnings (the repo currently has pre-existing warnings in other files, which can be ignored).
|
||||
|
||||
## No external assets
|
||||
|
||||
Data Controller must run entirely locally (offline / on-prem, no internet access). The built product must never fetch assets from remote servers — no external fonts, images, scripts, stylesheets, CDN links, or remote URLs in meta tags (e.g. `og:image`, `og:url`, `itemprop="image"`). All assets must be bundled and served locally.
|
||||
|
||||
## The .agent folder
|
||||
|
||||
Agent-related content lives in `.agent/`: technical/agent-facing documentation goes in `.agent/docs/` (not `docs/`), and skills in `.agent/skills/`. When writing explanatory or technical docs about the codebase, put them in `.agent/docs/`.
|
||||
@@ -157,5 +157,31 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +192,170 @@ context('editor tests: ', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// REGEX_HARD_COL/REGEX_SOFT_COL only exist un-stripped on MPE_X_NEW (see
|
||||
// RULE_DEMO_COLS in the getdata.js mock) — MPE_X_TEST stays clean to
|
||||
// match the excel fixtures.
|
||||
it('7 | Blocks submission of a value that fails HARDREGEX', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
scrollGridRight()
|
||||
|
||||
getCellByHeaderAndRow(1, 'REGEX_HARD_COL')
|
||||
.dblclick({ force: true })
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.clear()
|
||||
.type('not-an-email{enter}')
|
||||
.then(() => {
|
||||
submitTable(() => {
|
||||
cy.get('.modal-body').then((modalBody: any) => {
|
||||
if (
|
||||
modalBody[0].innerHTML
|
||||
.toLowerCase()
|
||||
.includes(`invalid values are present`)
|
||||
) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('8 | Warns (but still submits) a value that fails SOFTREGEX', (done) => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
scrollGridRight()
|
||||
|
||||
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL')
|
||||
.dblclick({ force: true })
|
||||
.then(() => {
|
||||
cy.focused()
|
||||
.clear()
|
||||
.type('not a postcode{enter}')
|
||||
.then(() => {
|
||||
getCellByHeaderAndRow(1, 'REGEX_SOFT_COL').should(
|
||||
'have.class',
|
||||
'dc-warning-cell'
|
||||
)
|
||||
|
||||
submitTable(() => {
|
||||
// Validation passed despite the SOFTREGEX warning: the
|
||||
// confirm-submit modal's Submit button is enabled
|
||||
// (validationDone === 1), not the "invalid values" abort.
|
||||
cy.get('#submitBtn', { timeout: longerCommandTimeout })
|
||||
.should('exist')
|
||||
.should('not.be.disabled')
|
||||
.then(() => done())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('9 | Info dropdown shows the applied HARDREGEX/SOFTREGEX pattern', () => {
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_new')
|
||||
|
||||
clickOnEdit(() => {
|
||||
cy.get('.btn.btn-sm.btn-icon.btn-outline-danger', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then(() => {
|
||||
scrollGridRight()
|
||||
|
||||
openColumnDropdown('REGEX_HARD_COL')
|
||||
cy.get('.htDropdownMenu').should(($menu) => {
|
||||
expect($menu.text()).to.include('REGEX: /[\\w.]+@[\\w]+\\.[a-z]{2,}/')
|
||||
})
|
||||
cy.get('body').click(0, 0) // close menu
|
||||
|
||||
openColumnDropdown('REGEX_SOFT_COL')
|
||||
cy.get('.htDropdownMenu').should(($menu) => {
|
||||
expect($menu.text()).to.include(
|
||||
'REGEX: /[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Handsontable virtualizes columns — with 17 columns on MPE_X_NEW, only
|
||||
// the ones in the current viewport actually exist in the DOM. Scroll all
|
||||
// the way right so REGEX_HARD_COL/REGEX_SOFT_COL (the last two) render at
|
||||
// all. Same technique already used in excel.cy.ts. Must be called (and
|
||||
// re-settle) before any header/body query below, since scrolling replaces
|
||||
// the previously-rendered column nodes.
|
||||
const scrollGridRight = () => {
|
||||
return cy
|
||||
.get('#hotTable')
|
||||
.find('div.ht_master.handsontable')
|
||||
.find('div.wtHolder')
|
||||
.scrollTo('right')
|
||||
}
|
||||
|
||||
// Opens a column header's dropdown menu to reach its `info` item, which
|
||||
// has a custom renderer showing NAME/LABEL/TYPE/LENGTH/FORMAT and, when the
|
||||
// column has a HARDREGEX/SOFTREGEX rule, the applied pattern. Clicking the
|
||||
// header text first selects the column - the renderer reads
|
||||
// hot.getSelected() to decide which column to describe.
|
||||
const openColumnDropdown = (headerText: string) => {
|
||||
cy.get('#hotTable .ht_clone_top .htCore thead button.changeType', {
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.parents('th')
|
||||
.filter((_, th) => Cypress.$(th).text().includes(headerText))
|
||||
.last()
|
||||
.as('targetHeader')
|
||||
|
||||
cy.get('@targetHeader').click()
|
||||
cy.get('@targetHeader').find('button.changeType').click({ force: true })
|
||||
}
|
||||
|
||||
// Locates a body cell by its column's header text rather than a hardcoded
|
||||
// childNodes index. Handsontable's hiddenColumns plugin (e.g. HIDDEN_COL,
|
||||
// used by several demo columns ahead of REGEX_HARD_COL/REGEX_SOFT_COL in
|
||||
// COLHEADERS) removes a hidden column's cells from BOTH the header row and
|
||||
// every body row identically, so the header row's th index always lines up
|
||||
// with the same-position childNode in any body row — this sidesteps having
|
||||
// to know how many hidden columns precede the target. Re-queries both the
|
||||
// header and body row fresh each call (rather than accepting a pre-fetched
|
||||
// row) since virtualization/scrolling can replace either pane's nodes.
|
||||
//
|
||||
// The `.should()` retries until headerText actually shows up: Handsontable
|
||||
// re-renders virtualized columns asynchronously after a scroll event, on
|
||||
// its own schedule outside Cypress's command queue, so scrollTo() settling
|
||||
// does not mean the target column has been rendered yet.
|
||||
const getCellByHeaderAndRow = (rowIndex: number, headerText: string) => {
|
||||
return cy
|
||||
.get('.ht_clone_top .htCore thead tr th')
|
||||
.should(($ths) => {
|
||||
const texts = [...$ths].map((th) => th.innerText.trim())
|
||||
expect(texts).to.include(headerText)
|
||||
})
|
||||
.then(($ths) => {
|
||||
const index = [...$ths].findIndex(
|
||||
(th) => th.innerText.trim() === headerText
|
||||
)
|
||||
|
||||
return cy
|
||||
.get('.ht_master tbody tr')
|
||||
.then((rows: any) => rows[rowIndex].childNodes[index])
|
||||
.then((cell) => cy.get(cell))
|
||||
})
|
||||
}
|
||||
|
||||
// Opens the _____DELETE__THIS__RECORD_____ dropdown editor on the given cell
|
||||
// and picks the given choice ('Yes'/'No') — same technique as
|
||||
// coltype-delete-record.cy.ts (arrow -> autocompleteEditor choice list).
|
||||
|
||||
+232
-238
@@ -41,16 +41,13 @@ context('editor tests: ', function () {
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
cy.contains('.open-viewbox', viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
|
||||
checkColumns(columns, () => {
|
||||
done()
|
||||
})
|
||||
}
|
||||
checkColumns(columns, () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,48 +75,21 @@ context('editor tests: ', function () {
|
||||
libraryToOpenIncludes,
|
||||
viewboxes.map((viewbox) => viewbox.viewbox_table)
|
||||
)
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
let found = 0
|
||||
viewboxes.forEach((viewbox) => {
|
||||
cy.contains('.open-viewbox', viewbox.viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
})
|
||||
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
for (let viewbox of viewboxes) {
|
||||
if (
|
||||
viewboxNode.innerText.toLowerCase().includes(viewbox.viewbox_table)
|
||||
)
|
||||
found++
|
||||
}
|
||||
}
|
||||
let result = 0
|
||||
|
||||
if (found < viewboxes.length) return
|
||||
viewboxes.forEach((viewbox) => {
|
||||
checkColumns(viewbox.columns, () => {
|
||||
result++
|
||||
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.table-title').then((tableTitle) => {
|
||||
const title = tableTitle[0].innerText
|
||||
const viewbox = viewboxes.find((vb) =>
|
||||
title.toLowerCase().includes(vb.viewbox_table)
|
||||
)
|
||||
|
||||
if (viewbox) {
|
||||
cy.get('.ht_master.handsontable .htCore thead tr').then(
|
||||
(viewboxColNodes: any) => {
|
||||
let allColsHtml = viewboxColNodes[0].innerHTML
|
||||
|
||||
for (let col of viewbox?.columns) {
|
||||
if (!allColsHtml.includes(col)) return
|
||||
}
|
||||
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
if (result === viewboxes.length) done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -133,22 +103,19 @@ context('editor tests: ', function () {
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
cy.contains('.open-viewbox', viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
removeAllColumns()
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns)
|
||||
addColumns(additionalColumns)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns], () => {
|
||||
done()
|
||||
})
|
||||
}
|
||||
checkColumns([...columns, ...additionalColumns], () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -162,35 +129,32 @@ context('editor tests: ', function () {
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
cy.contains('.open-viewbox', viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
|
||||
openViewboxConfig(viewbox_table)
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
removeAllColumns()
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
|
||||
cy.wait(500)
|
||||
cy.wait(500)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -204,53 +168,47 @@ context('editor tests: ', function () {
|
||||
cy.get('.viewbox-open').click()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
|
||||
cy.get('.open-viewbox').then((viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
if (!viewboxNode.innerText.toLowerCase().includes(viewbox_table)) {
|
||||
return
|
||||
}
|
||||
cy.contains('.open-viewbox', viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
|
||||
viewboxNode.click()
|
||||
openViewboxConfig(viewbox_table)
|
||||
|
||||
removeAllColumns()
|
||||
removeAllColumns()
|
||||
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
addColumns(additionalColumns, () => {
|
||||
cy.wait(1000)
|
||||
//reorder
|
||||
cy.get('.col-box.column-MOVE_TYPE')
|
||||
.realMouseDown({ button: 'left', position: 'center' })
|
||||
.realMouseMove(0, 10, { position: 'center' })
|
||||
cy.wait(200) // In our case, we wait 200ms cause we have animations which we are sure that take this amount of time
|
||||
cy.get('.col-box.column-IS_PK')
|
||||
.realMouseMove(0, 0, { position: 'center' })
|
||||
.realMouseUp()
|
||||
//reorder end
|
||||
|
||||
cy.wait(500)
|
||||
cy.wait(500)
|
||||
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
const colToRemove = 'MOVE_TYPE'
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
const colToRemove = 'MOVE_TYPE'
|
||||
|
||||
removeColumn(colToRemove)
|
||||
checkColumns(
|
||||
[
|
||||
...columns,
|
||||
...additionalColumns.filter((col) => col !== colToRemove)
|
||||
],
|
||||
() => {
|
||||
addColumns([colToRemove], () => {
|
||||
checkColumns(
|
||||
[...columns, ...additionalColumns.reverse()],
|
||||
() => {
|
||||
done()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
removeColumn(colToRemove)
|
||||
checkColumns(
|
||||
[
|
||||
...columns,
|
||||
...additionalColumns.filter((col) => col !== colToRemove)
|
||||
],
|
||||
() => {
|
||||
addColumns([colToRemove], () => {
|
||||
checkColumns([...columns, ...additionalColumns.reverse()], () => {
|
||||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -333,68 +291,68 @@ context('editor tests: ', function () {
|
||||
})
|
||||
})
|
||||
|
||||
// We will enable this test when we figure out how to mock filtering
|
||||
// it('7 | Add viewboxes and filter', () => {
|
||||
// const viewboxes = ['mpe_x_test', 'mpe_validations']
|
||||
// The base VIEW table and a viewbox on a different table must never
|
||||
// share a filter dialog's cached state - each keeps its own entry in
|
||||
// the globals filter cache (getFilterObjPath), keyed by viewboxId where
|
||||
// one is present. Opened from the VIEW page specifically
|
||||
// (rootParam === 'view') - the one branch where this previously broke,
|
||||
// since a 'view'-page QueryComponent was always treated as the base
|
||||
// table regardless of viewboxId.
|
||||
it('7 | Base table filter and viewbox filter stay isolated from each other', () => {
|
||||
const viewbox_table = 'mpe_audit'
|
||||
|
||||
// openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
visitPage('view/data')
|
||||
openTableFromTree(libraryToOpenIncludes, 'mpe_x_test')
|
||||
|
||||
// cy.get('.viewbox-open').click()
|
||||
// openTableFromViewboxTree(libraryToOpenIncludes, viewboxes)
|
||||
// Filter the base table.
|
||||
openViewFilter()
|
||||
setFilterWithValue('SOME_CHAR', 'this is dummy data', 'value', () => {
|
||||
cy.get('app-query', { withinSubject: null }).should('not.exist')
|
||||
|
||||
// cy.wait(1000)
|
||||
// Open a viewbox on a *different* table and check its filter dialog
|
||||
// does not show the base table's filter.
|
||||
openViewViewboxes()
|
||||
openTableFromViewboxTree(libraryToOpenIncludes, [viewbox_table])
|
||||
closeViewboxModal()
|
||||
|
||||
// closeViewboxModal()
|
||||
// cy.contains retries until the viewbox's title actually shows the
|
||||
// expected table name (it can render before its data/title loads),
|
||||
// then .parents() walks back up to its container - avoids a
|
||||
// one-shot .then() racing the title's own async update, which is
|
||||
// what made this flaky in the first place.
|
||||
cy.contains('.viewboxes-container .viewbox .table-title', viewbox_table, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.parents('.viewbox')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('clr-icon[shape="filter"]').click()
|
||||
|
||||
// cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
// (viewboxNodes: any) => {
|
||||
// for (let viewboxNode of viewboxNodes) {
|
||||
// cy.get(viewboxNode).within(() => {
|
||||
// cy.get('.table-title').then((title: any) => {
|
||||
// cy.get('.hot-spinner')
|
||||
// .should('not.exist')
|
||||
// .then(() => {
|
||||
// cy.get('clr-icon[shape="filter"]').then((filterButton) => {
|
||||
// filterButton[0].click()
|
||||
// })
|
||||
cy.get('.filter-modal code.language-sql', {
|
||||
withinSubject: null
|
||||
}).should(($code) => {
|
||||
expect($code.text()).to.not.contain('SOME_CHAR')
|
||||
})
|
||||
|
||||
// if (title[0].innerText.includes('MPE_X_TEST')) {
|
||||
// setFilterWithValue(
|
||||
// 'SOME_CHAR',
|
||||
// 'this is dummy data',
|
||||
// 'value',
|
||||
// () => {
|
||||
// cy.get('app-query', { withinSubject: null })
|
||||
// .should('not.exist')
|
||||
// .get('.ht_master.handsontable tbody tr')
|
||||
// .then((rowNodes) => {
|
||||
// const tr = rowNodes[0]
|
||||
// Reverse direction: filter the viewbox, then confirm
|
||||
// re-opening the base table's own filter dialog still shows
|
||||
// the base table's filter, not the viewbox's.
|
||||
setFilterWithValue('LIBREF', 'sasdemo', 'value', () => {
|
||||
cy.get('app-query', { withinSubject: null }).should('not.exist')
|
||||
|
||||
// expect(rowNodes).to.have.length(1)
|
||||
// expect(tr.innerText).to.equal('0')
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
// } else if (title[0].innerText.includes('MPE_VALIDATIONS')) {
|
||||
// setFilterWithValue('BASE_COL', 'ALERT_LIB', 'value', () => {
|
||||
// cy.get('app-query', { withinSubject: null })
|
||||
// .should('not.exist')
|
||||
// .get('.ht_master.handsontable tbody tr')
|
||||
// .then((rowNodes) => {
|
||||
// const tr = rowNodes[0]
|
||||
openViewFilter()
|
||||
|
||||
// expect(rowNodes).to.have.length(1)
|
||||
// expect(tr.innerText).to.contain('ALERT_LIB')
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
cy.get('.filter-modal code.language-sql', {
|
||||
withinSubject: null
|
||||
}).should(($code) => {
|
||||
expect($code.text()).to.contain('SOME_CHAR')
|
||||
expect($code.text()).to.not.contain('LIBREF')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const removeAllColumns = () => {
|
||||
@@ -407,32 +365,40 @@ const removeAllColumns = () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Asserts that at least one open viewbox currently shows exactly this
|
||||
// column list, in order. Uses a single .should() callback (synchronous
|
||||
// jQuery reads only, no nested cy.get()/.then()) so Cypress retries the
|
||||
// whole check against a fresh DOM snapshot until it passes or times out -
|
||||
// unlike .then(), which only ever sees one snapshot and, combined with an
|
||||
// early return on a mismatch, would silently never invoke the caller's
|
||||
// callback while Handsontable/the viewbox's data are still loading or
|
||||
// re-rendering asynchronously.
|
||||
const checkColumns = (columns: string[], callback: () => void) => {
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).then(
|
||||
(viewboxNodes: any) => {
|
||||
for (let viewboxNode of viewboxNodes) {
|
||||
cy.get(viewboxNode).within(() => {
|
||||
cy.get('.ht_master.handsontable thead tr th').then(
|
||||
(viewboxColNodes: any) => {
|
||||
console.log('viewboxColNode', viewboxColNodes)
|
||||
console.log('columns', columns)
|
||||
for (let i = 0; i < viewboxColNodes.length; i++) {
|
||||
const col = columns[i] || ''
|
||||
const colNode = viewboxColNodes[i]
|
||||
cy.get('.viewboxes-container .viewbox', { withinSubject: null }).should(
|
||||
($viewboxNodes) => {
|
||||
const matches = $viewboxNodes.toArray().some((viewboxNode) => {
|
||||
// rowHeaders: true (viewboxes.component.ts) adds a leading, unlabeled
|
||||
// corner <th> before the actual data columns - skip it, or every
|
||||
// comparison is off by one against the row-header cell instead of
|
||||
// the first real column.
|
||||
const headerCells = Cypress.$(viewboxNode)
|
||||
.find('.ht_master.handsontable thead tr th')
|
||||
.toArray()
|
||||
.slice(1)
|
||||
|
||||
if (
|
||||
!colNode.innerHTML.toLowerCase().includes(col.toLowerCase())
|
||||
)
|
||||
return
|
||||
}
|
||||
return columns.every((col, i) =>
|
||||
(headerCells[i]?.innerHTML || '')
|
||||
.toLowerCase()
|
||||
.includes(col.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
callback()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
expect(matches, `a viewbox showing columns: ${columns.join(', ')}`).to.be
|
||||
.true
|
||||
}
|
||||
)
|
||||
|
||||
cy.then(() => callback())
|
||||
}
|
||||
|
||||
const closeViewboxModal = () => {
|
||||
@@ -444,9 +410,7 @@ const removeColumn = (column: string) => {
|
||||
}
|
||||
|
||||
const addColumns = (columns: string[], callback?: () => void) => {
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const column = columns[i]
|
||||
|
||||
for (const column of columns) {
|
||||
cy.get('.cols-search input', { withinSubject: null }).type(column)
|
||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
@@ -454,51 +418,81 @@ const addColumns = (columns: string[], callback?: () => void) => {
|
||||
cy.get('.cols-search .autocomplete-wrapper', { withinSubject: null })
|
||||
.first()
|
||||
.trigger('keydown', { key: 'Enter' })
|
||||
.then(() => {
|
||||
if (i === columns.length - 1 && callback) callback()
|
||||
})
|
||||
|
||||
// Wait for the actual column box to render before moving on to the
|
||||
// next column (or firing the callback) - selecting it doesn't add it
|
||||
// to the DOM synchronously, and cy.get()'s own retry covers that gap
|
||||
// instead of relying on a fixed cy.wait().
|
||||
cy.get(`.col-box.column-${column}`, { timeout: longerCommandTimeout })
|
||||
}
|
||||
|
||||
if (callback) cy.then(() => callback())
|
||||
}
|
||||
|
||||
// The VIEW page's "options" dropdown (viewer.component.html) - distinct
|
||||
// from the editor page's dedicated .viewbox-open button used by the
|
||||
// tests above, since test 7 runs on the VIEW page specifically.
|
||||
const openViewOptionsDropdown = () => {
|
||||
cy.get('.filterSide', { withinSubject: null }).click()
|
||||
}
|
||||
|
||||
const openViewFilter = () => {
|
||||
openViewOptionsDropdown()
|
||||
cy.get('[clrDropdownItem]', { withinSubject: null })
|
||||
.contains('Filter')
|
||||
.click()
|
||||
}
|
||||
|
||||
const openViewViewboxes = () => {
|
||||
openViewOptionsDropdown()
|
||||
cy.get('[clrDropdownItem]', { withinSubject: null })
|
||||
.contains('Viewboxes')
|
||||
.click()
|
||||
}
|
||||
|
||||
// cy.contains retries until a matching node appears/updates - the open
|
||||
// viewbox's own list entry can render before its table-name text has
|
||||
// actually loaded, so a one-shot .then() lookup here would race it.
|
||||
const openViewboxConfig = (viewbox_tablename: string) => {
|
||||
cy.get('.open-viewbox').then((viewboxes: any) => {
|
||||
for (let openViewbox of viewboxes) {
|
||||
if (openViewbox.innerText.toLowerCase().includes(viewbox_tablename))
|
||||
openViewbox.click()
|
||||
}
|
||||
})
|
||||
cy.contains('.open-viewbox', viewbox_tablename, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
}).click()
|
||||
}
|
||||
|
||||
const openTableFromTree = (libNameIncludes: string, tablename: string) => {
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout })
|
||||
.should('not.exist')
|
||||
.then(() => {
|
||||
cy.get('.nav-tree clr-tree > clr-tree-node', {
|
||||
timeout: longerCommandTimeout
|
||||
}).then((treeNodes: any) => {
|
||||
let viyaLib
|
||||
cy.get('.app-loading', { timeout: longerCommandTimeout }).should('not.exist')
|
||||
|
||||
for (let node of treeNodes) {
|
||||
if (new RegExp(libNameIncludes).test(node.innerText.toLowerCase())) {
|
||||
viyaLib = node
|
||||
break
|
||||
}
|
||||
}
|
||||
// Small settle wait: right after a fresh visit (an extra navigation on
|
||||
// top of beforeEach's own, e.g. test 7's visitPage('view/data')), the
|
||||
// tree component can still be re-rendering, causing a node to be found
|
||||
// then swapped out mid-click - same fix already proven in
|
||||
// viewer-labels.cy.ts.
|
||||
cy.wait(300)
|
||||
|
||||
cy.get(viyaLib).within(() => {
|
||||
cy.get('.clr-tree-node-content-container p').click()
|
||||
const libraryNodeSelector = '.nav-tree clr-tree > clr-tree-node'
|
||||
const libraryMatcher = new RegExp(libNameIncludes, 'i')
|
||||
|
||||
cy.get('.clr-treenode-link').then((innerNodes: any) => {
|
||||
for (let innerNode of innerNodes) {
|
||||
if (innerNode.innerText.toLowerCase().includes(tablename)) {
|
||||
innerNode.click()
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.contains(libraryNodeSelector, libraryMatcher, {
|
||||
timeout: longerCommandTimeout
|
||||
})
|
||||
.find('.clr-tree-node-content-container p')
|
||||
.click()
|
||||
|
||||
// Re-query the library node fresh rather than reusing the reference
|
||||
// from before the click above - expanding a library can replace its
|
||||
// whole DOM subtree once its child tables load (that's what made the
|
||||
// earlier cached-reference version of this helper flaky: Cypress
|
||||
// detected the click's own side effect detaching the element it was
|
||||
// still verifying actionability against).
|
||||
cy.contains(libraryNodeSelector, libraryMatcher, {
|
||||
timeout: longerCommandTimeout
|
||||
}).within(() => {
|
||||
cy.contains('.clr-treenode-link', tablename, {
|
||||
matchCase: false,
|
||||
timeout: longerCommandTimeout
|
||||
}).click()
|
||||
})
|
||||
}
|
||||
|
||||
const setFilterWithValue = (
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ const check = (cwd) => {
|
||||
start: cwd,
|
||||
excludePrivatePackages: true,
|
||||
onlyAllow:
|
||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||
'AFLv2.1;Apache 2.0;Apache-2.0;Apache*;Artistic-2.0;BlueOak-1.0.0;0BSD;BSD*;BSD-2-Clause;BSD-3-Clause;CC0-1.0;CC-BY-3.0;CC-BY-4.0;ISC;MIT;MPL-2.0;ODC-By-1.0;Python-2.0;Unlicense;',
|
||||
excludePackages:
|
||||
'@cds/city@1.1.0;@handsontable/angular-wrapper@16.0.1;@handsontable/angular-wrapper@17.1.0;@handsontable/angular-wrapper@18.0.0;handsontable@^16.0.1;handsontable@16.2.0;handsontable@17.1.0;handsontable@18.0.0;hyperformula@2.7.1;hyperformula@3.0.0;hyperformula@3.1.0;hyperformula@3.2.0;hyperformula@3.3.0;jackspeak@3.4.3;path-scurry@1.11.1;package-json-from-dist@1.0.1;buffers@0.1.1'
|
||||
},
|
||||
|
||||
Generated
+6247
-6466
File diff suppressed because it is too large
Load Diff
+32
-23
@@ -41,28 +41,30 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.20",
|
||||
"@angular/cdk": "^19.2.19",
|
||||
"@angular/common": "^19.2.20",
|
||||
"@angular/compiler": "^19.2.20",
|
||||
"@angular/core": "^19.2.20",
|
||||
"@angular/forms": "^19.2.20",
|
||||
"@angular/platform-browser": "^19.2.20",
|
||||
"@angular/platform-browser-dynamic": "^19.2.20",
|
||||
"@angular/router": "^19.2.20",
|
||||
"@angular/animations": "^20.3.26",
|
||||
"@angular/cdk": "^20.2.14",
|
||||
"@angular/common": "^20.3.26",
|
||||
"@angular/compiler": "^20.3.26",
|
||||
"@angular/core": "^20.3.26",
|
||||
"@angular/forms": "^20.3.26",
|
||||
"@angular/platform-browser": "^20.3.26",
|
||||
"@angular/platform-browser-dynamic": "^20.3.26",
|
||||
"@angular/router": "^20.3.26",
|
||||
"@cds/core": "^6.15.1",
|
||||
"@clr/angular": "file:libraries/clr-angular-17.9.0.tgz",
|
||||
"@clr/icons": "^13.0.2",
|
||||
"@clr/ui": "file:libraries/clr-ui-17.9.0.tgz",
|
||||
"@handsontable/angular-wrapper": "^18.0.0",
|
||||
"@sasjs/adapter": "^4.17.0",
|
||||
"@sasjs/utils": "^3.5.3",
|
||||
"@sasjs/adapter": "^4.17.3",
|
||||
"@sasjs/utils": "^3.5.9",
|
||||
"@sheet/crypto": "file:libraries/sheet-crypto.tgz",
|
||||
"@types/d3-graphviz": "^2.6.7",
|
||||
"@types/text-encoding": "0.0.35",
|
||||
"base64-arraybuffer": "^0.2.0",
|
||||
"browserify-cipher": "^1.0.1",
|
||||
"buffer": "^5.4.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"create-hash": "^1.2.0",
|
||||
"create-hmac": "^1.1.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3-graphviz": "^5.0.2",
|
||||
"exceljs": "^4.4.0",
|
||||
@@ -77,8 +79,8 @@
|
||||
"moment": "^2.30.1",
|
||||
"ngx-clipboard": "^16.0.0",
|
||||
"ngx-json-viewer": "file:libraries/ngx-json-viewer-3.2.1.tgz",
|
||||
"nodejs": "0.0.0",
|
||||
"os-browserify": "0.3.0",
|
||||
"randombytes": "^2.1.0",
|
||||
"rxjs": "^7.8.0",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"stream-browserify": "3.0.0",
|
||||
@@ -91,29 +93,32 @@
|
||||
"zone.js": "~0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.24",
|
||||
"@angular-devkit/build-angular": "^20.3.32",
|
||||
"@angular-eslint/builder": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin": "19.8.1",
|
||||
"@angular-eslint/eslint-plugin-template": "19.8.1",
|
||||
"@angular-eslint/schematics": "19.8.1",
|
||||
"@angular-eslint/template-parser": "19.8.1",
|
||||
"@angular/cli": "^19.2.24",
|
||||
"@angular/compiler-cli": "^19.2.20",
|
||||
"@angular/cli": "^20.3.32",
|
||||
"@angular/compiler-cli": "^20.3.26",
|
||||
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
||||
"@compodoc/compodoc": "^1.2.1",
|
||||
"@compodoc/compodoc": "^2.0.0",
|
||||
"@cypress/webpack-preprocessor": "^5.17.1",
|
||||
"@lhci/cli": "^0.15.1",
|
||||
"@types/core-js": "^2.5.5",
|
||||
"@types/create-hash": "^1.2.6",
|
||||
"@types/create-hmac": "^1.1.3",
|
||||
"@types/crypto-js": "^4.2.1",
|
||||
"@types/es6-shim": "^0.31.39",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
"@types/lodash-es": "^4.17.3",
|
||||
"@types/marked": "^4.3.0",
|
||||
"@types/node": "12.20.50",
|
||||
"@typescript-eslint/eslint-plugin": "8.31.1",
|
||||
"@typescript-eslint/parser": "8.31.1",
|
||||
"@types/randombytes": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"core-js": "^2.5.4",
|
||||
"cypress": "^15.14.2",
|
||||
"cypress": "^15.19.0",
|
||||
"cypress-file-upload": "^5.0.8",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"cypress-real-events": "^1.8.1",
|
||||
@@ -135,13 +140,17 @@
|
||||
"rimraf": "3.0.2",
|
||||
"ts-loader": "^9.2.8",
|
||||
"ts-node": "^3.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"wait-on": "^6.0.1",
|
||||
"typescript": "~5.9.3",
|
||||
"wait-on": "^9.0.10",
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"ajv": "8.18.0",
|
||||
"uuid": "11.1.1",
|
||||
"lighthouse": "13.4.0"
|
||||
"lighthouse": "13.4.0",
|
||||
"exceljs": {
|
||||
"archiver": "^8.0.0",
|
||||
"unzipper": "^0.12.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+281
-283
@@ -1,292 +1,287 @@
|
||||
<div class="main-container">
|
||||
<ng-container *ngIf="!router.url.includes('licensing')">
|
||||
<div
|
||||
*ngIf="
|
||||
freeTierBanner && (!licenseExpiringDays || licenseExpiringDays < 0)
|
||||
"
|
||||
class="alert alert-app-level alert-warning"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<ng-container *ngIf="licenceProblem.value === null">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - to upgrade contact
|
||||
<contact-link classes="color-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a routerLink="/licensing/update" class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="licenceProblem.value !== null">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - Problem with licence
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
(click)="licenceProblemDetails(licenceProblem.value)"
|
||||
class="update-key cursor-pointer"
|
||||
>More details</a
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="licenseExpiringDays && !freeTierBanner"
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
This license key will expire in {{ licenseExpiringDays }}
|
||||
{{ licenseExpiringDays === 1 ? 'day' : 'days' }}. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ngIf="!freeTierBanner"
|
||||
routerLink="/licensing/update"
|
||||
class="update-key"
|
||||
>Update Licence Key</a
|
||||
@if (!router.url.includes('licensing')) {
|
||||
@if (freeTierBanner && (!licenseExpiringDays || licenseExpiringDays < 0)) {
|
||||
<div
|
||||
class="alert alert-app-level alert-warning"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="appOverCapacity"
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
@if (licenceProblem.value === null) {
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon
|
||||
class="alert-icon"
|
||||
shape="warning-standard"
|
||||
></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - to upgrade contact
|
||||
<contact-link classes="color-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert-text">
|
||||
The registered number of users exceeds the limit specified for your
|
||||
license. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
*ngIf="!licenseExpiringDays && !freeTierBanner"
|
||||
routerLink="/licensing/update"
|
||||
class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<header class="app-header" *ngIf="!embed">
|
||||
<!-- <button
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
"
|
||||
class="header-hamburger-trigger"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
>
|
||||
<span></span>
|
||||
</button> -->
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
aria-label="Toggle sidebar"
|
||||
class="cursor-pointer select-none ml-10 d-flex clr-justify-content-center clr-align-items-center"
|
||||
>
|
||||
<clr-icon size="24" shape="tree-view" aria-hidden="true"></clr-icon>
|
||||
</div>
|
||||
|
||||
<div class="logo d-flex clr-align-items-center">
|
||||
<a
|
||||
*ngIf="!router.url.includes('deploy')"
|
||||
href="#"
|
||||
[routerLink]="['/']"
|
||||
class="nav-link"
|
||||
>
|
||||
<img
|
||||
class="without-text d-block d-md-none"
|
||||
src="images/dc-logo.svg"
|
||||
alt="datacontroller logo without text"
|
||||
/>
|
||||
<img
|
||||
class="with-text d-none d-md-block"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a *ngIf="router.url.includes('deploy')">
|
||||
<span class="clr-icon header-logo ml-10"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!router.url.includes('deploy') && !router.url.includes('licensing')
|
||||
"
|
||||
>
|
||||
<div class="header-nav d-flex d-sm-none">
|
||||
<clr-dropdown>
|
||||
<button
|
||||
class="nav-icon color-white-i"
|
||||
clrDropdownTrigger
|
||||
aria-label="toggle settings menu"
|
||||
<a routerLink="/licensing/update" class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
Menu
|
||||
<!-- <clr-icon size="20" shape="bars"></clr-icon> -->
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
||||
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
||||
<a [routerLink]="['/home']" clrDropdownItem>LOAD</a>
|
||||
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
}
|
||||
@if (licenceProblem.value !== null) {
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon
|
||||
class="alert-icon"
|
||||
shape="warning-standard"
|
||||
></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
Data Controller (FREE Tier) - Problem with licence
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
(click)="licenceProblemDetails(licenceProblem.value)"
|
||||
class="update-key cursor-pointer"
|
||||
>More details</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="header-nav d-none d-sm-flex">
|
||||
<a
|
||||
[routerLink]="['/view']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>VIEW</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/home']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="
|
||||
router.url.includes('editor') ||
|
||||
router.url.includes('edit-record') ||
|
||||
router.url.includes('home')
|
||||
"
|
||||
>LOAD</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/review/submitted']"
|
||||
[class.active]="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history')
|
||||
"
|
||||
class="nav-link nav-text cursor-pointer"
|
||||
>REVIEW</a
|
||||
>
|
||||
}
|
||||
@if (licenseExpiringDays && !freeTierBanner) {
|
||||
<div
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
This license key will expire in {{ licenseExpiringDays }}
|
||||
{{ licenseExpiringDays === 1 ? 'day' : 'days' }}. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (!freeTierBanner) {
|
||||
<a routerLink="/licensing/update" class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
@if (appOverCapacity) {
|
||||
<div
|
||||
class="alert alert-app-level alert-danger"
|
||||
id="demo-banner"
|
||||
role="alert"
|
||||
>
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<cds-icon class="alert-icon" shape="warning-standard"></cds-icon>
|
||||
</div>
|
||||
<div class="alert-text">
|
||||
The registered number of users exceeds the limit specified for
|
||||
your license. Please contact
|
||||
<contact-link classes="color-white" />
|
||||
or your reseller to arrange additional licence for site id
|
||||
{{ syssite.getValue() }}.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (!licenseExpiringDays && !freeTierBanner) {
|
||||
<a routerLink="/licensing/update" class="update-key"
|
||||
>Update Licence Key</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<app-header-actions></app-header-actions>
|
||||
</header>
|
||||
<nav
|
||||
*ngIf="
|
||||
!embed &&
|
||||
(router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history'))
|
||||
"
|
||||
class="subnav"
|
||||
>
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/submitted']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>SUBMIT</a
|
||||
@if (!embed) {
|
||||
<header class="app-header">
|
||||
<!-- <button
|
||||
*ngIf="
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
"
|
||||
class="header-hamburger-trigger"
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
>
|
||||
<span></span>
|
||||
</button> -->
|
||||
@if (
|
||||
isMainRoute('view') ||
|
||||
(isMainRoute('home') && !router.url.includes('licensing'))
|
||||
) {
|
||||
<div
|
||||
(click)="toggleSidebar()"
|
||||
type="button"
|
||||
aria-label="Toggle sidebar"
|
||||
class="cursor-pointer select-none ml-10 d-flex clr-justify-content-center clr-align-items-center"
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/approve']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="router.url.includes('approve')"
|
||||
routerLinkActive="active"
|
||||
>APPROVE</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/history']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>HISTORY</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<clr-icon size="24" shape="tree-view" aria-hidden="true"></clr-icon>
|
||||
</div>
|
||||
}
|
||||
<div class="logo d-flex clr-align-items-center">
|
||||
@if (!router.url.includes('deploy')) {
|
||||
<a href="#" [routerLink]="['/']" class="nav-link">
|
||||
<img
|
||||
class="without-text d-block d-md-none"
|
||||
src="images/dc-logo.svg"
|
||||
alt="datacontroller logo without text"
|
||||
/>
|
||||
<img
|
||||
class="with-text d-none d-md-block"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
@if (router.url.includes('deploy')) {
|
||||
<a>
|
||||
<span class="clr-icon header-logo ml-10"></span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@if (
|
||||
!router.url.includes('deploy') && !router.url.includes('licensing')
|
||||
) {
|
||||
<div class="header-nav d-flex d-sm-none">
|
||||
<clr-dropdown>
|
||||
<button
|
||||
class="nav-icon color-white-i"
|
||||
clrDropdownTrigger
|
||||
aria-label="toggle settings menu"
|
||||
>
|
||||
Menu
|
||||
<!-- <clr-icon size="20" shape="bars"></clr-icon> -->
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen clrPosition="bottom-left">
|
||||
<a [routerLink]="['/view']" clrDropdownItem>VIEW</a>
|
||||
<a [routerLink]="['/home']" clrDropdownItem>LOAD</a>
|
||||
<a [routerLink]="['/review/submitted']" clrDropdownItem>REVIEW</a>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<div class="header-nav d-none d-sm-flex">
|
||||
<a
|
||||
[routerLink]="['/view']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>VIEW</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/home']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="
|
||||
router.url.includes('editor') ||
|
||||
router.url.includes('edit-record') ||
|
||||
router.url.includes('home')
|
||||
"
|
||||
>LOAD</a
|
||||
>
|
||||
<a
|
||||
[routerLink]="['/review/submitted']"
|
||||
[class.active]="
|
||||
router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history')
|
||||
"
|
||||
class="nav-link nav-text cursor-pointer"
|
||||
>REVIEW</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
<app-header-actions></app-header-actions>
|
||||
</header>
|
||||
}
|
||||
@if (
|
||||
!embed &&
|
||||
(router.url.includes('submitted') ||
|
||||
router.url.includes('approve') ||
|
||||
router.url.includes('history'))
|
||||
) {
|
||||
<nav class="subnav">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/submitted']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>SUBMIT</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/approve']"
|
||||
class="nav-link nav-text"
|
||||
[class.active]="router.url.includes('approve')"
|
||||
routerLinkActive="active"
|
||||
>APPROVE</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
[routerLink]="['/review/history']"
|
||||
class="nav-link nav-text"
|
||||
routerLinkActive="active"
|
||||
>HISTORY</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
|
||||
<app-alerts *ngIf="!errTop"></app-alerts>
|
||||
@if (!errTop) {
|
||||
<app-alerts></app-alerts>
|
||||
}
|
||||
<app-requests-modal [(opened)]="requestsModal"></app-requests-modal>
|
||||
<app-excel-password-modal></app-excel-password-modal>
|
||||
|
||||
<!-- <app-terms *ngIf="showRegistration"></app-terms> -->
|
||||
|
||||
<!-- VA embed, back to Editor button -->
|
||||
<div
|
||||
*ngIf="embed === 'va' && vaEditorLibds && !isMainRoute('/editor')"
|
||||
class="va-back-bar"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="backToEditor()"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="caret"
|
||||
dir="left"
|
||||
size="16"
|
||||
></clr-icon>
|
||||
Back to Edit table
|
||||
</button>
|
||||
</div>
|
||||
@if (embed === 'va' && vaEditorLibds && !isMainRoute('/editor')) {
|
||||
<div class="va-back-bar">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="backToEditor()"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="caret"
|
||||
dir="left"
|
||||
size="16"
|
||||
></clr-icon>
|
||||
Back to Edit table
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<router-outlet *ngIf="startupDataLoaded"></router-outlet>
|
||||
@if (startupDataLoaded) {
|
||||
<router-outlet></router-outlet>
|
||||
}
|
||||
|
||||
<app-login></app-login>
|
||||
<app-alerts *ngIf="errTop"></app-alerts>
|
||||
<app-info-modal
|
||||
*ngFor="let abort of sasjsAborts"
|
||||
[data]="abort"
|
||||
[forceReload]="!startupDataLoaded && sasjsAborts.length === 1"
|
||||
(onConfirmModalClick)="closeAbortModal(abort.id!)"
|
||||
>
|
||||
</app-info-modal>
|
||||
@if (errTop) {
|
||||
<app-alerts></app-alerts>
|
||||
}
|
||||
@for (abort of sasjsAborts; track abort) {
|
||||
<app-info-modal
|
||||
[data]="abort"
|
||||
[forceReload]="!startupDataLoaded && sasjsAborts.length === 1"
|
||||
(onConfirmModalClick)="closeAbortModal(abort.id!)"
|
||||
>
|
||||
</app-info-modal>
|
||||
}
|
||||
|
||||
<clr-modal
|
||||
appDragNdrop
|
||||
@@ -309,17 +304,20 @@
|
||||
</div>
|
||||
|
||||
<!-- App Loading Page -->
|
||||
<div *ngIf="!startupDataLoaded" class="app-loading">
|
||||
<img
|
||||
class="loading-logo"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
|
||||
<div *ngIf="appActive === null" class="slider">
|
||||
<div class="line"></div>
|
||||
<div class="subline inc"></div>
|
||||
<div class="subline dec"></div>
|
||||
@if (!startupDataLoaded) {
|
||||
<div class="app-loading">
|
||||
<img
|
||||
class="loading-logo"
|
||||
src="images/datacontroller.svg"
|
||||
alt="datacontroller logo"
|
||||
/>
|
||||
@if (appActive === null) {
|
||||
<div class="slider">
|
||||
<div class="line"></div>
|
||||
<div class="subline inc"></div>
|
||||
<div class="subline dec"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- /App Loading Page -->
|
||||
|
||||
@@ -1,100 +1,101 @@
|
||||
<main class="content-area position-relative">
|
||||
<div class="clr-row">
|
||||
<!-- T&C section -->
|
||||
<div *ngIf="step === 0" id="TCS" class="card">
|
||||
<div class="card-header">Terms and Conditions</div>
|
||||
<div class="card-block">
|
||||
<div class="card-text">
|
||||
<p class="mt-0">
|
||||
The Demo version of Data Controller is free for EVALUATION purposes
|
||||
only. Before proceeding with configuration, please confirm that you
|
||||
have read, understood, and agreed to the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
>.
|
||||
</p>
|
||||
@if (step === 0) {
|
||||
<div id="TCS" class="card">
|
||||
<div class="card-header">Terms and Conditions</div>
|
||||
<div class="card-block">
|
||||
<div class="card-text">
|
||||
<p class="mt-0">
|
||||
The Demo version of Data Controller is free for EVALUATION
|
||||
purposes only. Before proceeding with configuration, please
|
||||
confirm that you have read, understood, and agreed to the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<hr class="light" />
|
||||
<clr-checkbox-wrapper>
|
||||
<input clrCheckbox type="checkbox" (change)="termsAgreeChange()" />
|
||||
<label
|
||||
>I have read and agree to the terms of the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
></label
|
||||
>
|
||||
</clr-checkbox-wrapper>
|
||||
<!-- <hr />
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input
|
||||
[(ngModel)]="autodeploy"
|
||||
type="checkbox"
|
||||
id="checkbox2"
|
||||
class="clr-checkbox"
|
||||
checked
|
||||
/>
|
||||
<label for="checkbox2"
|
||||
>Autodeploy
|
||||
{{ !jsonFile ? '(json file is not available)' : '' }}</label
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<hr class="light" />
|
||||
|
||||
<clr-checkbox-wrapper>
|
||||
<input clrCheckbox type="checkbox" (change)="termsAgreeChange()" />
|
||||
<label
|
||||
>I have read and agree to the terms of the
|
||||
<a
|
||||
href="https://docs.datacontroller.io/evaluation-licence-agreement"
|
||||
target="_blank"
|
||||
>Data Controller for SAS© Evaluation Agreement</a
|
||||
></label
|
||||
>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<!-- <hr />
|
||||
|
||||
<div class="clr-checkbox-wrapper">
|
||||
<input
|
||||
[(ngModel)]="autodeploy"
|
||||
type="checkbox"
|
||||
id="checkbox2"
|
||||
class="clr-checkbox"
|
||||
checked
|
||||
/>
|
||||
<label for="checkbox2"
|
||||
>Autodeploy
|
||||
{{ !jsonFile ? '(json file is not available)' : '' }}</label
|
||||
>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- T&C section end -->
|
||||
|
||||
<ng-container *ngIf="step > 0" [ngSwitch]="true">
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.SasViya">
|
||||
<div *ngIf="autodeploy" class="autodeploy-section card">
|
||||
<app-automatic-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
[appLoc]="appLoc"
|
||||
[dcPath]="dcPath"
|
||||
[selectedAdminGroup]="selectedAdminGroup"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-automatic-deploy>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!autodeploy" id="mainbody" class="card">
|
||||
<app-manual-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-manual-deploy>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.Sasjs">
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="sasJsConfig.serverType === ServerType.Sas9">
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@if (step > 0) {
|
||||
@switch (true) {
|
||||
@case (sasJsConfig.serverType === ServerType.SasViya) {
|
||||
@if (autodeploy) {
|
||||
<div class="autodeploy-section card">
|
||||
<app-automatic-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
[appLoc]="appLoc"
|
||||
[dcPath]="dcPath"
|
||||
[selectedAdminGroup]="selectedAdminGroup"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-automatic-deploy>
|
||||
</div>
|
||||
}
|
||||
@if (!autodeploy) {
|
||||
<div id="mainbody" class="card">
|
||||
<app-manual-deploy
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-manual-deploy>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@case (sasJsConfig.serverType === ServerType.Sasjs) {
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
}
|
||||
@case (sasJsConfig.serverType === ServerType.Sas9) {
|
||||
<div class="autodeploy-section card">
|
||||
<app-sasjs-configurator
|
||||
[sasJs]="sasJs"
|
||||
[sasJsConfig]="sasJsConfig"
|
||||
[dcAdapterSettings]="dcAdapterSettings"
|
||||
(onNavigateToHome)="onNavigateToHome()"
|
||||
></app-sasjs-configurator>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</main>
|
||||
|
||||
@@ -1,91 +1,80 @@
|
||||
<div *ngIf="autodeploying" class="auto-deploy">
|
||||
<div class="spinner-box">
|
||||
<ng-container *ngIf="!autodeployDone">
|
||||
<span class="spinner spinner-md"> Loading... </span>
|
||||
<p>Deploying...</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="autodeployDone">
|
||||
<p class="m-0 align-self-start">Done</p>
|
||||
<hr class="w-100" />
|
||||
|
||||
<div
|
||||
*ngIf="autoDeployStatus.deployServicePack !== null"
|
||||
class="deploy-status-row"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.deployServicePack === true"
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="!autoDeployStatus.deployServicePack === false"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
<p>Deploy SAS Jobs</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="autoDeployStatus.runMakeData !== null"
|
||||
class="deploy-status-row"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.runMakeData"
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="autoDeployStatus.runMakeData === false"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
<p>Create database</p>
|
||||
</div>
|
||||
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="buttons">
|
||||
<button (click)="navigateToHome()" class="btn btn-primary mt-15 mr-0">
|
||||
<clr-icon
|
||||
*ngIf="
|
||||
@if (autodeploying) {
|
||||
<div class="auto-deploy">
|
||||
<div class="spinner-box">
|
||||
@if (!autodeployDone) {
|
||||
<span class="spinner spinner-md"> Loading... </span>
|
||||
<p>Deploying...</p>
|
||||
}
|
||||
@if (autodeployDone) {
|
||||
<p class="m-0 align-self-start">Done</p>
|
||||
<hr class="w-100" />
|
||||
@if (autoDeployStatus.deployServicePack !== null) {
|
||||
<div class="deploy-status-row">
|
||||
@if (autoDeployStatus.deployServicePack === true) {
|
||||
<clr-icon
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
}
|
||||
@if (!autoDeployStatus.deployServicePack === false) {
|
||||
<clr-icon class="deploy-error" shape="times-circle"></clr-icon>
|
||||
}
|
||||
<p>Deploy SAS Jobs</p>
|
||||
</div>
|
||||
}
|
||||
@if (autoDeployStatus.runMakeData !== null) {
|
||||
<div class="deploy-status-row">
|
||||
@if (autoDeployStatus.runMakeData) {
|
||||
<clr-icon
|
||||
class="deploy-success"
|
||||
shape="success-standard"
|
||||
></clr-icon>
|
||||
}
|
||||
@if (autoDeployStatus.runMakeData === false) {
|
||||
<clr-icon class="deploy-error" shape="times-circle"></clr-icon>
|
||||
}
|
||||
<p>Create database</p>
|
||||
</div>
|
||||
}
|
||||
<hr class="w-100" />
|
||||
<div class="buttons">
|
||||
<button (click)="navigateToHome()" class="btn btn-primary mt-15 mr-0">
|
||||
@if (
|
||||
autoDeployStatus.deployServicePack === false ||
|
||||
autoDeployStatus.runMakeData === false
|
||||
) {
|
||||
<clr-icon class="deploy-error" shape="times-circle"></clr-icon>
|
||||
}
|
||||
LAUNCH
|
||||
</button>
|
||||
<button
|
||||
(click)="
|
||||
downloadFile(makeDataResponse, 'create-database-log', 'txt')
|
||||
"
|
||||
class="deploy-error"
|
||||
shape="times-circle"
|
||||
></clr-icon>
|
||||
LAUNCH
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="downloadFile(makeDataResponse, 'create-database-log', 'txt')"
|
||||
class="btn btn-primary-outline mt-15 mr-0"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="w-100" />
|
||||
|
||||
<div class="buttons">
|
||||
<button
|
||||
(click)="autodeploying = false; autodeployDone = false"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="openSasRequestsModal()"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
SAS Requests
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
class="btn btn-primary-outline mt-15 mr-0"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
<hr class="w-100" />
|
||||
<div class="buttons">
|
||||
<button
|
||||
(click)="autodeploying = false; autodeployDone = false"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
(click)="openSasRequestsModal()"
|
||||
class="btn btn-primary-outline mt-15 mr-0 align-self-end"
|
||||
>
|
||||
SAS Requests
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h4 class="text-center my-15">Viya Deploy</h4>
|
||||
<hr />
|
||||
@@ -106,21 +95,18 @@
|
||||
<label for="dcloc" class="mt-20 clr-control-label">SAS Admin group</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<select
|
||||
*ngIf="!adminGroupsLoading"
|
||||
clrSelect
|
||||
name="options"
|
||||
[(ngModel)]="selectedAdminGroup"
|
||||
>
|
||||
<option *ngFor="let adminGroup of adminGroups" [value]="adminGroup.id">
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
</select>
|
||||
<clr-spinner
|
||||
clrInline
|
||||
class="spinner-sm"
|
||||
*ngIf="adminGroupsLoading"
|
||||
></clr-spinner>
|
||||
@if (!adminGroupsLoading) {
|
||||
<select clrSelect name="options" [(ngModel)]="selectedAdminGroup">
|
||||
@for (adminGroup of adminGroups; track adminGroup) {
|
||||
<option [value]="adminGroup.id">
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
@if (adminGroupsLoading) {
|
||||
<clr-spinner clrInline class="spinner-sm"></clr-spinner>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,36 +115,34 @@
|
||||
>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper small-mt">
|
||||
<select
|
||||
*ngIf="!computeContextsLoading"
|
||||
clrSelect
|
||||
name="options"
|
||||
(ngModelChange)="onComputeContextChange($event)"
|
||||
[(ngModel)]="selectedComputeContext"
|
||||
>
|
||||
<option
|
||||
*ngFor="let computeContext of computeContexts"
|
||||
[value]="computeContext.id"
|
||||
@if (!computeContextsLoading) {
|
||||
<select
|
||||
clrSelect
|
||||
name="options"
|
||||
(ngModelChange)="onComputeContextChange($event)"
|
||||
[(ngModel)]="selectedComputeContext"
|
||||
>
|
||||
{{ computeContext.name }}
|
||||
</option>
|
||||
</select>
|
||||
<clr-spinner
|
||||
clrInline
|
||||
class="spinner-sm"
|
||||
*ngIf="computeContextsLoading"
|
||||
></clr-spinner>
|
||||
@for (computeContext of computeContexts; track computeContext) {
|
||||
<option [value]="computeContext.id">
|
||||
{{ computeContext.name }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
@if (computeContextsLoading) {
|
||||
<clr-spinner clrInline class="spinner-sm"></clr-spinner>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="runningAsUser">
|
||||
@if (runningAsUser) {
|
||||
<label for="dcloc" class="mt-20 clr-control-label">Running as user:</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<p class="mt-0">{{ runningAsUser }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
|
||||
|
||||
@@ -169,7 +153,7 @@
|
||||
(click)="recreateDatabaseClicked($event)"
|
||||
type="checkbox"
|
||||
checked
|
||||
/>
|
||||
/>
|
||||
<label>Recreate database</label>
|
||||
</clr-checkbox-wrapper> -->
|
||||
|
||||
@@ -185,18 +169,18 @@
|
||||
<!-- Keeping this for a reference in case future VIYA changes and starts allowing separate backend and frontend) -->
|
||||
|
||||
<!-- <button
|
||||
(click)="executeJson()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
[disabled]="!jsonFile"
|
||||
(click)="executeJson()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
[disabled]="!jsonFile"
|
||||
>
|
||||
Deploy {{ !jsonFile ? '(json file is not available)' : '' }}
|
||||
Deploy {{ !jsonFile ? '(json file is not available)' : '' }}
|
||||
</button> -->
|
||||
|
||||
<!-- <button
|
||||
(click)="uploadJsonAuto.click()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
(click)="uploadJsonAuto.click()"
|
||||
class="btn-autodeploy btn btn-primary d-inline-block mr-10"
|
||||
>
|
||||
Upload different file to deploy
|
||||
Upload different file to deploy
|
||||
</button>
|
||||
<input
|
||||
#uploadJsonAuto
|
||||
@@ -204,7 +188,7 @@
|
||||
hidden
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onJsonFileChange($event)"
|
||||
/> -->
|
||||
/> -->
|
||||
|
||||
<clr-modal [(clrModalOpen)]="recreateDatabaseModal" [clrModalClosable]="false">
|
||||
<h3 class="modal-title">Warning</h3>
|
||||
|
||||
@@ -10,29 +10,29 @@
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<!-- <div class="card-title">
|
||||
Client Details
|
||||
</div> -->
|
||||
Client Details
|
||||
</div> -->
|
||||
|
||||
<div *ngIf="needsLogin" id="loginForm" class="d-none">
|
||||
<p class="mb-10">Please log in first</p>
|
||||
<label for="username" class="clr-control-label">Username</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="text" id="username" class="clr-input" />
|
||||
@if (needsLogin) {
|
||||
<div id="loginForm" class="d-none">
|
||||
<p class="mb-10">Please log in first</p>
|
||||
<label for="username" class="clr-control-label">Username</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="text" id="username" class="clr-input" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="password" class="clr-control-label">Password</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="password" id="password" class="clr-input" />
|
||||
<label for="password" class="clr-control-label">Password</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input type="password" id="password" class="clr-input" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary d-none" id="loginBtn">Log in</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-primary d-none" id="loginBtn">Log in</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!needsLogin">
|
||||
@if (!needsLogin) {
|
||||
<form>
|
||||
<div class="clr-form-control">
|
||||
<label for="select-full" class="clr-control-label">Admin group</label>
|
||||
@@ -45,17 +45,15 @@
|
||||
id="adminGroupsSelect"
|
||||
class="clr-select"
|
||||
>
|
||||
<option
|
||||
*ngFor="let adminGroup of adminGroups"
|
||||
[value]="adminGroup.id"
|
||||
>
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
@for (adminGroup of adminGroups; track adminGroup) {
|
||||
<option [value]="adminGroup.id">
|
||||
{{ adminGroup.name }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-form-control">
|
||||
<div [class.hidden]="contextsLoading">
|
||||
<label for="select-full" class="clr-control-label">Context</label>
|
||||
@@ -67,15 +65,14 @@
|
||||
name="selectedContext"
|
||||
class="clr-select"
|
||||
>
|
||||
<option
|
||||
*ngFor="let context of allContexts"
|
||||
[value]="context.name"
|
||||
>
|
||||
{{ context.name }}
|
||||
<span *ngIf="(context.attributes | json) != '{}'"
|
||||
>( {{ context.attributes.sysUserId }} )</span
|
||||
>
|
||||
</option>
|
||||
@for (context of allContexts; track context) {
|
||||
<option [value]="context.name">
|
||||
{{ context.name }}
|
||||
@if ((context.attributes | json) != '{}') {
|
||||
<span>( {{ context.attributes.sysUserId }} )</span>
|
||||
}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,14 +85,12 @@
|
||||
<clr-icon shape="play"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div [class.hidden]="!contextsLoading" class="d-flex">
|
||||
<span class="spinner spinner-inline mr-10">
|
||||
Loading contexts...
|
||||
</span>
|
||||
<span> Loading contexts... </span>
|
||||
</div>
|
||||
|
||||
<label for="dcloc" class="mt-20 clr-control-label">DC Loc</label>
|
||||
<div class="mb-10 clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
@@ -110,7 +105,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-10">
|
||||
Select JSON file to upload (json build file preloaded):
|
||||
</p>
|
||||
@@ -121,10 +115,9 @@
|
||||
(change)="onJsonFileChange($event)"
|
||||
/>
|
||||
<!-- <button *ngIf="downloadFileBtn" (click)="downloadSasPrecodeFile()" style="width: 40px; min-width: 0;" class="btn btn-sm btn-icon">
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button> -->
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button> -->
|
||||
</div>
|
||||
|
||||
<div class="mt-20 d-flex align-items-center">
|
||||
<button
|
||||
(click)="executeJson()"
|
||||
@@ -135,13 +128,13 @@
|
||||
>
|
||||
SUBMIT JSON
|
||||
</button>
|
||||
<span *ngIf="isJsonSubmitted">JSON Submitted Successfully</span>
|
||||
|
||||
@if (isJsonSubmitted) {
|
||||
<span>JSON Submitted Successfully</span>
|
||||
}
|
||||
<!-- <span *ngIf="executingScript" class="spinner spinner-inline ml-3">
|
||||
Loading...
|
||||
</span> -->
|
||||
Loading...
|
||||
</span> -->
|
||||
</div>
|
||||
|
||||
<p class="mt-10">Select SAS file to upload:</p>
|
||||
<div class="d-flex flex-column">
|
||||
<input
|
||||
@@ -149,15 +142,15 @@
|
||||
(click)="clearUploadInput($event)"
|
||||
(change)="onSasFileChange($event); downloadFileBtn = true"
|
||||
/>
|
||||
<button
|
||||
*ngIf="downloadFileBtn"
|
||||
(click)="downloadSasPrecodeFile()"
|
||||
class="btn btn-sm btn-icon min-w-0 w-40"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
@if (downloadFileBtn) {
|
||||
<button
|
||||
(click)="downloadSasPrecodeFile()"
|
||||
class="btn btn-sm btn-icon min-w-0 w-40"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mt-20 d-flex align-items-center">
|
||||
<button
|
||||
(click)="executeSAS()"
|
||||
@@ -168,17 +161,14 @@
|
||||
>
|
||||
SUBMIT
|
||||
</button>
|
||||
|
||||
<!-- <span *ngIf="executingScript" class="spinner spinner-inline ml-3">
|
||||
Loading...
|
||||
</span> -->
|
||||
Loading...
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="jobLog.length > 0">
|
||||
@if (jobLog.length > 0) {
|
||||
<p class="mb-0 mt-10">File execute completed</p>
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<button
|
||||
(click)="downloadFile(jobLog, 'execute-script-log', 'txt')"
|
||||
@@ -187,8 +177,7 @@
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
}
|
||||
<button
|
||||
[clrLoading]="createDatabaseLoading"
|
||||
(click)="createDatabase()"
|
||||
@@ -196,52 +185,45 @@
|
||||
>
|
||||
Create Database
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="makeDataResponse.length > 0">
|
||||
@if (makeDataResponse.length > 0) {
|
||||
<p class="mb-0 mt-10">Create Database Completed</p>
|
||||
<hr />
|
||||
|
||||
<div *ngIf="makeDataResponse.length > 0" class="log-wrapper">
|
||||
{{ makeDataResponse }}
|
||||
</div>
|
||||
|
||||
@if (makeDataResponse.length > 0) {
|
||||
<div class="log-wrapper">
|
||||
{{ makeDataResponse }}
|
||||
</div>
|
||||
}
|
||||
<button (click)="navigateToHome()" class="btn btn-primary mt-15">
|
||||
Let's get started
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="downloadFile(makeDataResponse, 'create-database-log', 'txt')"
|
||||
class="btn btn-primary mt-15"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
|
||||
<button (click)="validateDeploy()" class="btn btn-primary mt-15">
|
||||
Validate
|
||||
</button>
|
||||
|
||||
<div
|
||||
*ngIf="validationState !== 'none' || isValidating"
|
||||
class="validation-bar"
|
||||
>
|
||||
<ng-container *ngIf="isValidating">
|
||||
<span class="spinner spinner-inline mr-10">
|
||||
Validating deploy...
|
||||
</span>
|
||||
<span> Validating deploy... </span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!isValidating && validationState === 'error'">
|
||||
<clr-icon shape="exclamation-circle" class="is-error"></clr-icon>
|
||||
<span> Validation failed </span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!isValidating && validationState === 'success'">
|
||||
<clr-icon shape="check-circle" class="is-success"></clr-icon>
|
||||
<span> Validation succeeded </span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
@if (validationState !== 'none' || isValidating) {
|
||||
<div class="validation-bar">
|
||||
@if (isValidating) {
|
||||
<span class="spinner spinner-inline mr-10">
|
||||
Validating deploy...
|
||||
</span>
|
||||
<span> Validating deploy... </span>
|
||||
}
|
||||
@if (!isValidating && validationState === 'error') {
|
||||
<clr-icon shape="exclamation-circle" class="is-error"></clr-icon>
|
||||
<span> Validation failed </span>
|
||||
}
|
||||
@if (!isValidating && validationState === 'success') {
|
||||
<clr-icon shape="check-circle" class="is-success"></clr-icon>
|
||||
<span> Validation succeeded </span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
|
||||
+17
-16
@@ -1,6 +1,8 @@
|
||||
<div *ngIf="loading" class="thinProgress progresStatic progress loop">
|
||||
<progress></progress>
|
||||
</div>
|
||||
@if (loading) {
|
||||
<div class="thinProgress progresStatic progress loop">
|
||||
<progress></progress>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h4 class="text-center my-15">Sasjs Deploy</h4>
|
||||
<hr class="light" />
|
||||
@@ -27,13 +29,13 @@
|
||||
</div>
|
||||
|
||||
<!-- <button
|
||||
(click)="directoryBrowse.click()"
|
||||
class="mt-15 text-center"
|
||||
class="btn btn-sm btn-outline"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
<input #directoryBrowse hidden (change)="dirChange($event)" type="file" id="ctrl" webkitdirectory directory multiple/> -->
|
||||
(click)="directoryBrowse.click()"
|
||||
class="mt-15 text-center"
|
||||
class="btn btn-sm btn-outline"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
<input #directoryBrowse hidden (change)="dirChange($event)" type="file" id="ctrl" webkitdirectory directory multiple/> -->
|
||||
</div>
|
||||
|
||||
<p class="m-0 mt-10">
|
||||
@@ -45,12 +47,11 @@
|
||||
<label class="mt-20 clr-control-label">Data Controller Admin group</label>
|
||||
<clr-select-container class="mb-10 mt-0 w-50vw">
|
||||
<select [(ngModel)]="dcAdminGroup" clrSelect>
|
||||
<option
|
||||
*ngFor="let adminGroup of dcAdminGroupList"
|
||||
[value]="adminGroup.GROUPNAME"
|
||||
>
|
||||
{{ adminGroup.GROUPNAME }} - {{ adminGroup.GROUPDESC }}
|
||||
</option>
|
||||
@for (adminGroup of dcAdminGroupList; track adminGroup) {
|
||||
<option [value]="adminGroup.GROUPNAME">
|
||||
{{ adminGroup.GROUPNAME }} - {{ adminGroup.GROUPDESC }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
|
||||
|
||||
@@ -84,6 +84,20 @@
|
||||
status="warning"
|
||||
></clr-icon>
|
||||
|
||||
<!-- SOFTREGEX: value doesn't match the expected pattern, but
|
||||
submission is still allowed (unlike HARDREGEX, which is
|
||||
surfaced via the existing invalid-data class below). -->
|
||||
<clr-icon
|
||||
*ngIf="
|
||||
!currentRecordErrors.includes(colIndex) &&
|
||||
currentRecordWarningCols.includes(col.key)
|
||||
"
|
||||
class="flex-unset position-absolute entry-input-left-offset"
|
||||
shape="error-standard"
|
||||
status="warning"
|
||||
title="Value does not match the expected pattern"
|
||||
></clr-icon>
|
||||
|
||||
<ng-container *ngSwitchCase="'numeric'">
|
||||
<clr-input-container
|
||||
*ngIf="
|
||||
|
||||
@@ -55,6 +55,7 @@ export class EditRecordComponent implements OnInit {
|
||||
@Output() onPreviousRecord: EventEmitter<any> = new EventEmitter<any>()
|
||||
|
||||
public currentRecordInvalidCols: string[] = []
|
||||
public currentRecordWarningCols: string[] = []
|
||||
public generateEditRecordUrlLoading: boolean = false
|
||||
public generatedRecordUrl: string | null = null
|
||||
public addRecordUrl: string | null = null
|
||||
@@ -120,9 +121,12 @@ export class EditRecordComponent implements OnInit {
|
||||
*/
|
||||
private revalidateRecordCol(colName: string, value: any) {
|
||||
const colRules = this.currentRecordValidator?.getRule(colName)
|
||||
|
||||
this.validateRecordCol(colRules, value).then((valid: boolean) =>
|
||||
this.updateValidationState(colName, valid)
|
||||
)
|
||||
|
||||
this.updateWarningState(colName, value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +225,8 @@ export class EditRecordComponent implements OnInit {
|
||||
this.tryAutoPopulateNotNull(event, colName, colRules, value)
|
||||
}
|
||||
})
|
||||
|
||||
this.updateWarningState(colName, value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,6 +243,22 @@ export class EditRecordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the SOFTREGEX warning columns list — the modal's equivalent of
|
||||
* makeRegexWarningRenderer in the grid (see DcValidator.failsSoftRegex).
|
||||
*/
|
||||
private updateWarningState(colName: string, value: any): void {
|
||||
const failsSoftRegex =
|
||||
this.currentRecordValidator?.failsSoftRegex(colName, value) ?? false
|
||||
const index = this.currentRecordWarningCols.indexOf(colName)
|
||||
|
||||
if (!failsSoftRegex && index > -1) {
|
||||
this.currentRecordWarningCols.splice(index, 1)
|
||||
} else if (failsSoftRegex && index < 0) {
|
||||
this.currentRecordWarningCols.push(colName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-populates NOTNULL default value when the field is empty and has a default
|
||||
*/
|
||||
@@ -264,6 +286,8 @@ export class EditRecordComponent implements OnInit {
|
||||
this.validateRecordCol(colRules, defaultValue).then((isValid: boolean) => {
|
||||
this.updateValidationState(colName, isValid)
|
||||
})
|
||||
|
||||
this.updateWarningState(colName, defaultValue)
|
||||
}
|
||||
|
||||
onNextRecordClick() {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<div>
|
||||
<p class="m-0" *ngFor="let state of processedStates; let i = index">
|
||||
{{ state }}
|
||||
</p>
|
||||
@for (state of processedStates; track state; let i = $index) {
|
||||
<p class="m-0">
|
||||
{{ state }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<span class="spinner spinner-sm vertical-align-middle">Loading...</span>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3254,7 +3254,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
colName = this.hotInstance?.colToProp(selectedCol) as string
|
||||
colInfo = this.$dataFormats?.vars[colName]
|
||||
|
||||
textInfo = buildColInfoHtml(colName, colInfo)
|
||||
textInfo = buildColInfoHtml(
|
||||
colName,
|
||||
colInfo,
|
||||
this.dcValidator?.getRegexRuleValue(colName)
|
||||
)
|
||||
}
|
||||
|
||||
elem.innerHTML = textInfo
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { makeRegexWarningRenderer } from './regex-warning-renderer'
|
||||
|
||||
describe('makeRegexWarningRenderer', () => {
|
||||
const buildHot = (data: any[]) => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
const hot = new Handsontable(container, {
|
||||
data,
|
||||
columns: [
|
||||
{ data: 'val', renderer: makeRegexWarningRenderer('^[A-Z]{3}$') },
|
||||
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||
],
|
||||
licenseKey: 'non-commercial-and-evaluation'
|
||||
})
|
||||
hot.render()
|
||||
|
||||
return { hot, container }
|
||||
}
|
||||
|
||||
it('adds dc-warning-cell when the value fails the pattern', () => {
|
||||
const { hot, container } = buildHot([
|
||||
{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }
|
||||
])
|
||||
|
||||
const td = hot.getCell(0, 0)
|
||||
expect(td?.classList.contains('dc-warning-cell')).toBeTrue()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('does not add dc-warning-cell when the value matches the pattern', () => {
|
||||
const { hot, container } = buildHot([
|
||||
{ val: 'ABC', _____DELETE__THIS__RECORD_____: 'No' }
|
||||
])
|
||||
|
||||
const td = hot.getCell(0, 0)
|
||||
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('suppresses the warning on a row marked for delete, even though the value fails the pattern', () => {
|
||||
const { hot, container } = buildHot([
|
||||
{ val: 'abc', _____DELETE__THIS__RECORD_____: 'Yes' }
|
||||
])
|
||||
|
||||
const td = hot.getCell(0, 0)
|
||||
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('does not add dc-warning-cell for a SAS special missing value', () => {
|
||||
const { hot, container } = buildHot([
|
||||
{ val: '.a', _____DELETE__THIS__RECORD_____: 'No' }
|
||||
])
|
||||
|
||||
const td = hot.getCell(0, 0)
|
||||
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('does not throw and never warns on a malformed pattern', () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
const hot = new Handsontable(container, {
|
||||
data: [{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }],
|
||||
columns: [
|
||||
{ data: 'val', renderer: makeRegexWarningRenderer('[unterminated') },
|
||||
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||
],
|
||||
licenseKey: 'non-commercial-and-evaluation'
|
||||
})
|
||||
|
||||
expect(() => hot.render()).not.toThrow()
|
||||
|
||||
const td = hot.getCell(0, 0)
|
||||
expect(td?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('handles a PRX-delimited pattern with a case-insensitive flag, as authored for prxparse', () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
|
||||
const hot = new Handsontable(container, {
|
||||
data: [
|
||||
{ val: 'this is dummy data', _____DELETE__THIS__RECORD_____: 'No' },
|
||||
{ val: 'THE WIND WAS BLOWING', _____DELETE__THIS__RECORD_____: 'No' },
|
||||
{ val: 'nothing relevant here', _____DELETE__THIS__RECORD_____: 'No' }
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
data: 'val',
|
||||
renderer: makeRegexWarningRenderer('/\\b(the|data)\\b/i')
|
||||
},
|
||||
{ data: '_____DELETE__THIS__RECORD_____' }
|
||||
],
|
||||
licenseKey: 'non-commercial-and-evaluation'
|
||||
})
|
||||
hot.render()
|
||||
|
||||
expect(hot.getCell(0, 0)?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
expect(hot.getCell(1, 0)?.classList.contains('dc-warning-cell')).toBeFalse()
|
||||
expect(hot.getCell(2, 0)?.classList.contains('dc-warning-cell')).toBeTrue()
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('is display-only — the stored value is untouched', () => {
|
||||
const { hot, container } = buildHot([
|
||||
{ val: 'abc', _____DELETE__THIS__RECORD_____: 'No' }
|
||||
])
|
||||
|
||||
expect(hot.getDataAtCell(0, 0)).toEqual('abc')
|
||||
|
||||
hot.destroy()
|
||||
container.remove()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
import Handsontable from 'handsontable'
|
||||
import { isRegexRuleExempt } from '../../shared/dc-validator/utils/isRegexRuleExempt'
|
||||
import { parseRegexRule } from '../../shared/dc-validator/utils/parseRegexRule'
|
||||
|
||||
/**
|
||||
* Builds a display-only HOT renderer for SOFTREGEX: a value that fails the
|
||||
* pattern gets a yellow `dc-warning-cell` class (styles.scss) instead of
|
||||
* blocking submission — SOFTREGEX must never return false from the cell
|
||||
* validator (that would paint htInvalid and block submit), so the warning
|
||||
* lives purely at the render layer, same split as makeNumberFormatRenderer.
|
||||
*
|
||||
* Suppressed on rows marked for delete (_____DELETE__THIS__RECORD_____ =
|
||||
* 'Yes') — a warning about data about to be removed is just noise.
|
||||
*
|
||||
* Falls back to no warning ever showing when the pattern itself is
|
||||
* malformed, rather than breaking the cell.
|
||||
*/
|
||||
export const makeRegexWarningRenderer = (pattern?: string) => {
|
||||
let regex: RegExp | null = null
|
||||
|
||||
try {
|
||||
if (pattern) regex = parseRegexRule(pattern)
|
||||
} catch (e) {
|
||||
console.warn(`SOFTREGEX - invalid pattern, warning disabled: ${pattern}`)
|
||||
regex = null
|
||||
}
|
||||
|
||||
const baseRenderer = Handsontable.renderers.getRenderer('text')
|
||||
|
||||
return (
|
||||
instance: any,
|
||||
td: any,
|
||||
row: number,
|
||||
col: number,
|
||||
prop: string | number,
|
||||
value: any,
|
||||
cellProperties: any
|
||||
) => {
|
||||
baseRenderer(instance, td, row, col, prop, value, cellProperties)
|
||||
|
||||
const markedForDelete =
|
||||
instance.getDataAtRowProp(row, '_____DELETE__THIS__RECORD_____') === 'Yes'
|
||||
|
||||
const failsPattern =
|
||||
!!regex && !isRegexRuleExempt(value) && !regex.test(value.toString())
|
||||
|
||||
if (failsPattern && !markedForDelete) {
|
||||
td.classList.add('dc-warning-cell')
|
||||
td.title = 'Value does not match the expected pattern'
|
||||
}
|
||||
|
||||
return td
|
||||
}
|
||||
}
|
||||
@@ -1,144 +1,167 @@
|
||||
<app-sidebar class="sidebar-height">
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="groups" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Groups"
|
||||
name="input"
|
||||
[(ngModel)]="groupSearch"
|
||||
(keyup)="groupListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="groupSearch = ''; groupListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<ng-container *ngFor="let group of groups">
|
||||
<clr-tree-node
|
||||
(click)="groupOnClick(group)"
|
||||
*ngIf="!group['hidden']"
|
||||
[class.active]="group.GROUPURI === groupUri"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="users"></clr-icon>
|
||||
{{ group.GROUPNAME }}
|
||||
</p>
|
||||
@if (groups) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Groups"
|
||||
name="input"
|
||||
[(ngModel)]="groupSearch"
|
||||
(keyup)="groupListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="groupSearch = ''; groupListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@for (group of groups; track group) {
|
||||
@if (!group['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="groupOnClick(group)"
|
||||
[class.active]="group.GROUPURI === groupUri"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="users"></clr-icon>
|
||||
{{ group.GROUPNAME }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
<div class="content-area">
|
||||
<div *ngIf="loading" class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
<div *ngIf="groupMembers && !loading">
|
||||
<div *ngIf="serverType !== ServerType.SasViya" class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table group-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="group-info-text">
|
||||
<b>{{ groupName }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<i>{{ groupDesc || 'no description' }}</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (loading) {
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card group-data">
|
||||
<div *ngIf="serverType !== ServerType.SasViya">
|
||||
<h3>MEMBERS ({{ groupMemberCount }})</h3>
|
||||
<h5 *ngIf="groupMemberCount == 0">No Members Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="groupMemberCount != 0" class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<ng-container *ngIf="serverType === ServerType.Sas9">
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="serverType === ServerType.Sasjs">
|
||||
<td class="width-25"><b>ID</b></td>
|
||||
<td class="width-25"><b>DISPLAY NAME</b></td>
|
||||
<td class="width-25"><b>USER NAME</b></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
[routerLink]="
|
||||
'/view/usernav/users/' + (member.URIMEM || member.ID)
|
||||
"
|
||||
*ngFor="let member of groupMembers"
|
||||
>
|
||||
<ng-container *ngIf="serverType === ServerType.Sas9">
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
<td class="">{{ member.EMAIL }}</td>
|
||||
<td class="">{{ member.MEMBERCREATED }}</td>
|
||||
<td class="">{{ member.MEMBERUPDATED }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="serverType === ServerType.Sasjs">
|
||||
<td class="">{{ member.ID }}</td>
|
||||
<td class="">{{ member.DISPLAYNAME }}</td>
|
||||
<td class="">{{ member.USERNAME }}</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (groupMembers && !loading) {
|
||||
<div>
|
||||
@if (serverType !== ServerType.SasViya) {
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table group-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="group-info-text">
|
||||
<b>{{ groupName }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<i>{{ groupDesc || 'no description' }}</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div *ngIf="serverType === ServerType.SasViya">
|
||||
<h3>{{ groupName }}</h3>
|
||||
<h5 *ngIf="groupMemberCount == 0">No Members Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="groupMemberCount != 0" class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<b>MEMBERS ({{ groupMemberCount }})</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
[routerLink]="'/view/usernav/users/' + member.MEMBERID"
|
||||
*ngFor="let member of groupMembers"
|
||||
>
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card group-data">
|
||||
@if (serverType !== ServerType.SasViya) {
|
||||
<div>
|
||||
<h3>MEMBERS ({{ groupMemberCount }})</h3>
|
||||
@if (groupMemberCount == 0) {
|
||||
<h5>No Members Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (groupMemberCount != 0) {
|
||||
<table class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
}
|
||||
@if (serverType === ServerType.Sasjs) {
|
||||
<td class="width-25"><b>ID</b></td>
|
||||
<td class="width-25"><b>DISPLAY NAME</b></td>
|
||||
<td class="width-25"><b>USER NAME</b></td>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (member of groupMembers; track member) {
|
||||
<tr
|
||||
[routerLink]="
|
||||
'/view/usernav/users/' +
|
||||
(member.URIMEM || member.ID)
|
||||
"
|
||||
>
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
<td class="">{{ member.EMAIL }}</td>
|
||||
<td class="">{{ member.MEMBERCREATED }}</td>
|
||||
<td class="">{{ member.MEMBERUPDATED }}</td>
|
||||
}
|
||||
@if (serverType === ServerType.Sasjs) {
|
||||
<td class="">{{ member.ID }}</td>
|
||||
<td class="">{{ member.DISPLAYNAME }}</td>
|
||||
<td class="">{{ member.USERNAME }}</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (serverType === ServerType.SasViya) {
|
||||
<div>
|
||||
<h3>{{ groupName }}</h3>
|
||||
@if (groupMemberCount == 0) {
|
||||
<h5>No Members Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (groupMemberCount != 0) {
|
||||
<table class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<b>MEMBERS ({{ groupMemberCount }})</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (member of groupMembers; track member) {
|
||||
<tr
|
||||
[routerLink]="
|
||||
'/view/usernav/users/' + member.MEMBERID
|
||||
"
|
||||
>
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,159 +1,158 @@
|
||||
<app-sidebar>
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="treeNodeLibraries?.length! > 0" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
clrInput
|
||||
appStealFocus
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
aria-label="Clear libraries search"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<ng-container *ngFor="let library of treeNodeLibraries">
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, library); lib = library.LIBRARYREF"
|
||||
*ngIf="!library['hidden']"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<p
|
||||
(click)="
|
||||
lib = library.LIBRARYREF;
|
||||
libraryOnClick(library.LIBRARYREF, library)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="rack-server" aria-hidden="true"></clr-icon>
|
||||
{{ library.LIBRARYREF }}
|
||||
</p>
|
||||
|
||||
<clr-tree-node *ngIf="library['tables']" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
||||
@if (treeNodeLibraries?.length! > 0) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
clrInput
|
||||
appStealFocus
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search" aria-hidden="true"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length > 0"
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables')
|
||||
"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
aria-label="Clear tables search"
|
||||
aria-label="Clear libraries search"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<clr-tree-node
|
||||
*ngFor="let libTable of library['tables']; let index = index"
|
||||
>
|
||||
<clr-tooltip
|
||||
*ngVar="
|
||||
index + 1 >
|
||||
licenceState.value.tables_in_library_limit as tableLocked
|
||||
"
|
||||
>
|
||||
<button
|
||||
clrTooltipTrigger
|
||||
(click)="!tableLocked ? onTableClick(libTable, library) : ''"
|
||||
class="clr-treenode-link"
|
||||
[class.dc-locked-control]="tableLocked"
|
||||
[class.active]="libTabActive(library.LIBRARYREF, libTable)"
|
||||
>
|
||||
<ng-container [ngSwitch]="libTable.includes('-FC')">
|
||||
<clr-icon
|
||||
*ngSwitchCase="true"
|
||||
shape="bolt"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngSwitchCase="false"
|
||||
shape="table"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
</ng-container>
|
||||
{{ libTable.replace('-FC', '') }}
|
||||
</button>
|
||||
|
||||
<ng-container *ngIf="tableLocked">
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
<span>
|
||||
To unlock all tables, contact support@datacontroller.io
|
||||
</span>
|
||||
</clr-tooltip-content>
|
||||
</ng-container>
|
||||
</clr-tooltip>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@for (library of treeNodeLibraries; track library) {
|
||||
@if (!library['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, library); lib = library.LIBRARYREF"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<p
|
||||
(click)="
|
||||
lib = library.LIBRARYREF;
|
||||
libraryOnClick(library.LIBRARYREF, library)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="rack-server" aria-hidden="true"></clr-icon>
|
||||
{{ library.LIBRARYREF }}
|
||||
</p>
|
||||
@if (library['tables']) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search" aria-hidden="true"></clr-icon>
|
||||
}
|
||||
@if (searchTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables')
|
||||
"
|
||||
shape="times"
|
||||
aria-label="Clear tables search"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
@for (
|
||||
libTable of library['tables'];
|
||||
track libTable;
|
||||
let index = $index
|
||||
) {
|
||||
<clr-tree-node>
|
||||
<clr-tooltip
|
||||
*ngVar="
|
||||
index + 1 >
|
||||
licenceState.value.tables_in_library_limit as tableLocked
|
||||
"
|
||||
>
|
||||
<button
|
||||
clrTooltipTrigger
|
||||
(click)="!tableLocked ? onTableClick(libTable, library) : ''"
|
||||
class="clr-treenode-link"
|
||||
[class.dc-locked-control]="tableLocked"
|
||||
[class.active]="libTabActive(library.LIBRARYREF, libTable)"
|
||||
>
|
||||
@switch (libTable.includes('-FC')) {
|
||||
@case (true) {
|
||||
<clr-icon shape="bolt" aria-hidden="true"></clr-icon>
|
||||
}
|
||||
@case (false) {
|
||||
<clr-icon shape="table" aria-hidden="true"></clr-icon>
|
||||
}
|
||||
}
|
||||
{{ libTable.replace('-FC', '') }}
|
||||
</button>
|
||||
@if (tableLocked) {
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
<span>
|
||||
To unlock all tables, contact
|
||||
support@datacontroller.io
|
||||
</span>
|
||||
</clr-tooltip-content>
|
||||
}
|
||||
</clr-tooltip>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
<main class="content-area">
|
||||
<div class="card-block">
|
||||
<div *ngIf="loading" class="spinner-wrapper-fullpage">
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
@if (loading) {
|
||||
<div class="spinner-wrapper-fullpage">
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="!loading" class="no-table-selected">
|
||||
<img
|
||||
src="images/select-table.png"
|
||||
class="select-table-icon"
|
||||
alt="select table icon"
|
||||
/>
|
||||
<p
|
||||
*ngIf="treeNodeLibraries?.length! > 0"
|
||||
class="text-center color-gray mt-10"
|
||||
cds-text="section"
|
||||
>
|
||||
Please select a table
|
||||
</p>
|
||||
<p
|
||||
*ngIf="treeNodeLibraries?.length! < 1"
|
||||
class="text-center color-gray mt-10"
|
||||
cds-text="section"
|
||||
>
|
||||
No Editable Tables Configured
|
||||
</p>
|
||||
</div>
|
||||
@if (!loading) {
|
||||
<div class="no-table-selected">
|
||||
<img
|
||||
src="images/select-table.png"
|
||||
class="select-table-icon"
|
||||
alt="select table icon"
|
||||
/>
|
||||
@if (treeNodeLibraries?.length! > 0) {
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a table
|
||||
</p>
|
||||
}
|
||||
@if (treeNodeLibraries?.length! < 1) {
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
No Editable Tables Configured
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,168 +1,176 @@
|
||||
<app-sidebar (scrolledToBottom)="loadMoreLibraries()">
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="libraryList" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<ng-container *ngFor="let library of libraryList">
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, library, libraryList)"
|
||||
*ngIf="!library['hidden'] && library['inForeground']"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<p
|
||||
(click)="lib = library.LIBRARYID; libraryOnClick(lib || '', library)"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
{{ library.LIBRARYNAME }}
|
||||
</p>
|
||||
|
||||
<clr-tree-node *ngIf="library['tables']" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables.TABLENAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (libraryList) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length > 0"
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables.TABLENAME')
|
||||
"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
|
||||
@for (library of libraryList; track library) {
|
||||
@if (!library['hidden'] && library['inForeground']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, libTable, library['tables'])"
|
||||
*ngFor="let libTable of library['tables']"
|
||||
[(clrExpanded)]="libTable['expanded']"
|
||||
[clrLoading]="libTable['loadingColumns'] && !libTable.columns"
|
||||
[class.clr-expanded]="libTable['expanded']"
|
||||
(click)="treeNodeClicked($event, library, libraryList)"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<p
|
||||
(click)="tableOnClick(libTable.TABLEURI, libTable, library)"
|
||||
[id]="libTable.TABLEURI"
|
||||
(click)="
|
||||
lib = library.LIBRARYID; libraryOnClick(lib || '', library)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="table"></clr-icon>
|
||||
{{ libTable.TABLENAME }}
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
{{ library.LIBRARYNAME }}
|
||||
</p>
|
||||
|
||||
<clr-tree-node *ngIf="libTable['columns']" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Columns"
|
||||
name="input"
|
||||
[(ngModel)]="libTable['searchString']"
|
||||
(keyup)="treeOnFilter(libTable, 'columns.COLNAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length > 0"
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
libTable['searchString'] = '';
|
||||
treeOnFilter(libTable, 'columns.COLNAME')
|
||||
"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<clr-tree-node *ngFor="let libColumn of libTable['columns']">
|
||||
<button
|
||||
(click)="columnOnClick(libColumn, library, libTable)"
|
||||
class="clr-treenode-link"
|
||||
[class.column-active]="libColumnActive(libColumn.COLURI)"
|
||||
@if (library['tables']) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables.TABLENAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables.TABLENAME')
|
||||
"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
@for (libTable of library['tables']; track libTable) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, libTable, library['tables'])"
|
||||
[(clrExpanded)]="libTable['expanded']"
|
||||
[clrLoading]="libTable['loadingColumns'] && !libTable.columns"
|
||||
[class.clr-expanded]="libTable['expanded']"
|
||||
>
|
||||
<clr-icon shape="objects"></clr-icon>
|
||||
|
||||
{{ libColumn.COLNAME }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
<p
|
||||
(click)="tableOnClick(libTable.TABLEURI, libTable, library)"
|
||||
[id]="libTable.TABLEURI"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="table"></clr-icon>
|
||||
{{ libTable.TABLENAME }}
|
||||
</p>
|
||||
@if (libTable['columns']) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Columns"
|
||||
name="input"
|
||||
[(ngModel)]="libTable['searchString']"
|
||||
(keyup)="treeOnFilter(libTable, 'columns.COLNAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
libTable['searchString'] = '';
|
||||
treeOnFilter(libTable, 'columns.COLNAME')
|
||||
"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
@for (libColumn of libTable['columns']; track libColumn) {
|
||||
<clr-tree-node>
|
||||
<button
|
||||
(click)="columnOnClick(libColumn, library, libTable)"
|
||||
class="clr-treenode-link"
|
||||
[class.column-active]="libColumnActive(libColumn.COLURI)"
|
||||
>
|
||||
<clr-icon shape="objects"></clr-icon>
|
||||
{{ libColumn.COLNAME }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
|
||||
<div *ngIf="librariesPaging" class="w-100 text-center">
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div>
|
||||
@if (librariesPaging) {
|
||||
<div class="w-100 text-center">
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div>
|
||||
}
|
||||
</app-sidebar>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="card">
|
||||
<div *ngIf="!column && !table" class="no-table-selected">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a column or table
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="column || table">
|
||||
<div
|
||||
*ngIf="!graphContainer"
|
||||
class="card-header d-flex flex-column justify-content-center"
|
||||
>
|
||||
<h3
|
||||
*ngIf="!currentLineagePathColumn && !currentLineagePathLibTable"
|
||||
class="text-center pb-10"
|
||||
>
|
||||
{{
|
||||
currentLineagePathColumn
|
||||
? currentLineagePathLibTable + '.' + currentLineagePathColumn
|
||||
: currentLineagePathLibTable
|
||||
}}
|
||||
</h3>
|
||||
@if (!column && !table) {
|
||||
<div class="no-table-selected">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a column or table
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (column || table) {
|
||||
@if (!graphContainer) {
|
||||
<div class="card-header d-flex flex-column justify-content-center">
|
||||
@if (!currentLineagePathColumn && !currentLineagePathLibTable) {
|
||||
<h3 class="text-center pb-10">
|
||||
{{
|
||||
currentLineagePathColumn
|
||||
? currentLineagePathLibTable + '.' + currentLineagePathColumn
|
||||
: currentLineagePathLibTable
|
||||
}}
|
||||
</h3>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="card-block">
|
||||
<section
|
||||
class="form-block sw position-relative d-flex align-items-center"
|
||||
@@ -170,114 +178,115 @@
|
||||
<div
|
||||
class="linage-title-wrapper d-flex align-items-center font-weight-bold position-absolute"
|
||||
>
|
||||
<span *ngIf="lineageTableName.length > 0">
|
||||
{{ lineageTableName.split('.')[0] }}.<a
|
||||
[routerLink]="'/view/data/' + lineageTableName"
|
||||
>{{ lineageTableName.split('.')[1] }}</a
|
||||
>{{ lineageColumnName.length > 0 ? '.' + lineageColumnName : '' }}
|
||||
</span>
|
||||
@if (lineageTableName.length > 0) {
|
||||
<span>
|
||||
{{ lineageTableName.split('.')[0] }}.<a
|
||||
[routerLink]="'/view/data/' + lineageTableName"
|
||||
>{{ lineageTableName.split('.')[1] }}</a
|
||||
>{{
|
||||
lineageColumnName.length > 0 ? '.' + lineageColumnName : ''
|
||||
}}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="graphContainer"
|
||||
class="clr-col-md-12 text-center d-flex justify-content-end"
|
||||
>
|
||||
<button
|
||||
(click)="limitDotDepth = true"
|
||||
type="button"
|
||||
class="btn btn-outline mr-5"
|
||||
>
|
||||
Limit depth
|
||||
</button>
|
||||
|
||||
<!-- <button class="btn btn-outline" (click)='showSvg()'> Open in New Tab </button> -->
|
||||
<div class="btn-group direction d-block">
|
||||
<div
|
||||
class="radio btn"
|
||||
(click)="
|
||||
forwardLineage = false;
|
||||
router.url.includes('column')
|
||||
? onGenerateClick()
|
||||
: onGenerateGraphTableClick()
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="btn-group-demo-radios"
|
||||
[checked]="!forwardLineage"
|
||||
/>
|
||||
<label>Backward</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="radio btn"
|
||||
(click)="
|
||||
forwardLineage = true;
|
||||
router.url.includes('column')
|
||||
? onGenerateClick()
|
||||
: onGenerateGraphTableClick()
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="btn-group-demo-radios"
|
||||
[checked]="forwardLineage"
|
||||
/>
|
||||
<label>Forward</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<clr-dropdown class="mr-10">
|
||||
@if (graphContainer) {
|
||||
<div class="clr-col-md-12 text-center d-flex justify-content-end">
|
||||
<button
|
||||
class="btn btn-info-outline"
|
||||
clrDropdownTrigger
|
||||
[disabled]="!column && !table"
|
||||
(click)="limitDotDepth = true"
|
||||
type="button"
|
||||
class="btn btn-outline mr-5"
|
||||
>
|
||||
Download
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
Limit depth
|
||||
</button>
|
||||
|
||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
||||
<div (click)="downloadPNG()" clrDropdownItem>PNG</div>
|
||||
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
||||
<div *ngIf="flatdata" (click)="downloadCSV()" clrDropdownItem>
|
||||
CSV
|
||||
<!-- <button class="btn btn-outline" (click)='showSvg()'> Open in New Tab </button> -->
|
||||
<div class="btn-group direction d-block">
|
||||
<div
|
||||
class="radio btn"
|
||||
(click)="
|
||||
forwardLineage = false;
|
||||
router.url.includes('column')
|
||||
? onGenerateClick()
|
||||
: onGenerateGraphTableClick()
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="btn-group-demo-radios"
|
||||
[checked]="!forwardLineage"
|
||||
/>
|
||||
<label>Backward</label>
|
||||
</div>
|
||||
<div
|
||||
class="radio btn"
|
||||
(click)="
|
||||
forwardLineage = true;
|
||||
router.url.includes('column')
|
||||
? onGenerateClick()
|
||||
: onGenerateGraphTableClick()
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="btn-group-demo-radios"
|
||||
[checked]="forwardLineage"
|
||||
/>
|
||||
<label>Forward</label>
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
|
||||
<clr-checkbox-wrapper
|
||||
*ngIf="column"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
(change)="generateGraph()"
|
||||
clrCheckbox
|
||||
name="refreshCache"
|
||||
[(ngModel)]="refreshCache"
|
||||
/>
|
||||
<label>Refresh Cache</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div *ngIf="graphContainer" [class.mt-2]="tableFlag">
|
||||
<div class="text-center">
|
||||
<span *ngIf="graphLoading" class="spinner"> Loading... </span>
|
||||
|
||||
<div *ngIf="!graphLoading" class="position-relative">
|
||||
<div class="graph-render-spinner">
|
||||
<span *ngIf="graphRendering" class="spinner spinner-sm"></span>
|
||||
</div>
|
||||
|
||||
<div id="graph"></div>
|
||||
<clr-dropdown class="mr-10">
|
||||
<button
|
||||
class="btn btn-info-outline"
|
||||
clrDropdownTrigger
|
||||
[disabled]="!column && !table"
|
||||
>
|
||||
Download
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div (click)="downloadSVG()" clrDropdownItem>SVG</div>
|
||||
<div (click)="downloadPNG()" clrDropdownItem>PNG</div>
|
||||
<div (click)="downloadDot()" clrDropdownItem>Dot</div>
|
||||
@if (flatdata) {
|
||||
<div (click)="downloadCSV()" clrDropdownItem>CSV</div>
|
||||
}
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
@if (column) {
|
||||
<clr-checkbox-wrapper class="d-flex align-items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
(change)="generateGraph()"
|
||||
clrCheckbox
|
||||
name="refreshCache"
|
||||
[(ngModel)]="refreshCache"
|
||||
/>
|
||||
<label>Refresh Cache</label>
|
||||
</clr-checkbox-wrapper>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@if (graphContainer) {
|
||||
<div [class.mt-2]="tableFlag">
|
||||
<div class="text-center">
|
||||
@if (graphLoading) {
|
||||
<span class="spinner"> Loading... </span>
|
||||
}
|
||||
@if (!graphLoading) {
|
||||
<div class="position-relative">
|
||||
<div class="graph-render-spinner">
|
||||
@if (graphRendering) {
|
||||
<span class="spinner spinner-sm"></span>
|
||||
}
|
||||
</div>
|
||||
<div id="graph"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,58 +8,60 @@
|
||||
[(ngModel)]="repository"
|
||||
(change)="updateSelectedRepository()"
|
||||
>
|
||||
<option
|
||||
*ngFor="let repository of repositories"
|
||||
value="{{ repository }}"
|
||||
>
|
||||
{{ repository }}
|
||||
</option>
|
||||
@for (repository of repositories; track repository) {
|
||||
<option value="{{ repository }}">
|
||||
{{ repository }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="metaDataList" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="search SAS Types"
|
||||
name="input"
|
||||
[(ngModel)]="metaDataSearch"
|
||||
(keyup)="metaListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="metaDataSearch = ''; metaListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
<ng-container *ngFor="let metaData of metaDataList">
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, metaData, metaDataList)"
|
||||
*ngIf="!metaData['hidden']"
|
||||
[(clrExpanded)]="metaData['expanded']"
|
||||
[clrLoading]="metaData['loadingTables'] && !metaData.tables"
|
||||
>
|
||||
<p
|
||||
(click)="
|
||||
metaDataId = metaData.ID; metaDataOnClick(metaDataId, metaData)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ metaData.ID }}
|
||||
</p>
|
||||
@if (metaDataList) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="search SAS Types"
|
||||
name="input"
|
||||
[(ngModel)]="metaDataSearch"
|
||||
(keyup)="metaListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="metaDataSearch = ''; metaListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
@for (metaData of metaDataList; track metaData) {
|
||||
@if (!metaData['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, metaData, metaDataList)"
|
||||
[(clrExpanded)]="metaData['expanded']"
|
||||
[clrLoading]="metaData['loadingTables'] && !metaData.tables"
|
||||
>
|
||||
<p
|
||||
(click)="
|
||||
metaDataId = metaData.ID; metaDataOnClick(metaDataId, metaData)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ metaData.ID }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
@@ -67,54 +69,121 @@
|
||||
<div class="card background-transparent-i">
|
||||
<h3 class="color-gray">{{ assoTypeSelected }}</h3>
|
||||
|
||||
<div *ngIf="!loading && !metaObjectList" class="no-table-selected">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a type
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="loadingSpinner" *ngIf="loading">
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div *ngIf="loading">
|
||||
<h4 *ngIf="metatypesLoading">Loading metadata types</h4>
|
||||
<h4 *ngIf="!metatypesLoading">Loading metadata objects</h4>
|
||||
@if (!loading && !metaObjectList) {
|
||||
<div class="no-table-selected">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a type
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showData" class="clr-row clr-flex-grow-1">
|
||||
<div class="clr-col-6">
|
||||
<div *ngIf="metaObjectList && !objectRoute" class="search-input">
|
||||
<input
|
||||
clrInput
|
||||
#searchObjTreeInput
|
||||
placeholder="search"
|
||||
name="input"
|
||||
[(ngModel)]="metaObjectSearch"
|
||||
(keyup)="metaObjectOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div *ngIf="!objectView" class="objects-col">
|
||||
<clr-accordion>
|
||||
<ng-container *ngFor="let metaObject of metaObjectShowList">
|
||||
<clr-accordion-panel
|
||||
(clrAccordionPanelOpenChange)="
|
||||
$event ? panelChange($event, metaObject) : ''
|
||||
"
|
||||
*ngIf="!metaObject['hidden']"
|
||||
>
|
||||
<clr-accordion-title
|
||||
><clr-icon shape="rack-server"></clr-icon>
|
||||
{{ metaObject.NAME }}
|
||||
<p class="float-right">{{ metaObject.ID }}</p>
|
||||
</clr-accordion-title>
|
||||
<clr-accordion-content *clrIfExpanded>
|
||||
<clr-tree *ngIf="showAcc" [clrLazy]="true">
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
@if (loading) {
|
||||
<div>
|
||||
@if (metatypesLoading) {
|
||||
<h4>Loading metadata types</h4>
|
||||
}
|
||||
@if (!metatypesLoading) {
|
||||
<h4>Loading metadata objects</h4>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (showData) {
|
||||
<div class="clr-row clr-flex-grow-1">
|
||||
<div class="clr-col-6">
|
||||
@if (metaObjectList && !objectRoute) {
|
||||
<div class="search-input">
|
||||
<input
|
||||
clrInput
|
||||
#searchObjTreeInput
|
||||
placeholder="search"
|
||||
name="input"
|
||||
[(ngModel)]="metaObjectSearch"
|
||||
(keyup)="metaObjectOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<br />
|
||||
@if (!objectView) {
|
||||
<div class="objects-col">
|
||||
<clr-accordion>
|
||||
@for (metaObject of metaObjectShowList; track metaObject) {
|
||||
@if (!metaObject['hidden']) {
|
||||
<clr-accordion-panel
|
||||
(clrAccordionPanelOpenChange)="
|
||||
$event ? panelChange($event, metaObject) : ''
|
||||
"
|
||||
>
|
||||
<clr-accordion-title
|
||||
><clr-icon shape="rack-server"></clr-icon>
|
||||
{{ metaObject.NAME }}
|
||||
<p class="float-right">{{ metaObject.ID }}</p>
|
||||
</clr-accordion-title>
|
||||
<clr-accordion-content *clrIfExpanded>
|
||||
@if (showAcc) {
|
||||
<clr-tree [clrLazy]="true">
|
||||
<clr-tree-node
|
||||
*clrRecursiveFor="
|
||||
let entry of root$ | async;
|
||||
getChildren: getChildren
|
||||
"
|
||||
[clrExpandable]="true"
|
||||
>
|
||||
<div
|
||||
[class.object-header]="!entry.count"
|
||||
class="full-width"
|
||||
>
|
||||
<div>
|
||||
@if (!entry.count) {
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
}
|
||||
@if (entry.count) {
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
}
|
||||
{{ entry.display }}
|
||||
</div>
|
||||
@if (!entry.count) {
|
||||
<p class="float-right object-uri">
|
||||
{{ entry.URI }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</clr-tree>
|
||||
}
|
||||
</clr-accordion-content>
|
||||
</clr-accordion-panel>
|
||||
}
|
||||
}
|
||||
</clr-accordion>
|
||||
</div>
|
||||
}
|
||||
@if (objectView) {
|
||||
<div class="objects-col">
|
||||
@for (metaObject of metaObjectShowList; track metaObject) {
|
||||
<div class="cols-head">
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
<div class="object-text">
|
||||
<p class="m-0 word-break mr-20">
|
||||
{{ metaObject.NAME }}
|
||||
</p>
|
||||
<p class="float-right ml-3">
|
||||
{{ metaObject.ID }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (showAcc) {
|
||||
<clr-tree [clrLazy]="true">
|
||||
<clr-tree-node
|
||||
*clrRecursiveFor="
|
||||
let entry of root$ | async;
|
||||
@@ -127,109 +196,70 @@
|
||||
class="full-width"
|
||||
>
|
||||
<div>
|
||||
<clr-icon
|
||||
*ngIf="!entry.count"
|
||||
shape="rack-server"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="entry.count"
|
||||
shape="block"
|
||||
></clr-icon>
|
||||
@if (!entry.count) {
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
}
|
||||
@if (entry.count) {
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
}
|
||||
{{ entry.display }}
|
||||
</div>
|
||||
|
||||
<p class="float-right object-uri" *ngIf="!entry.count">
|
||||
{{ entry.URI }}
|
||||
</p>
|
||||
@if (!entry.count) {
|
||||
<p class="float-right object-uri">
|
||||
{{ entry.URI }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</clr-tree>
|
||||
</clr-accordion-content>
|
||||
</clr-accordion-panel>
|
||||
</ng-container>
|
||||
</clr-accordion>
|
||||
</div>
|
||||
|
||||
<div *ngIf="objectView" class="objects-col">
|
||||
<ng-container *ngFor="let metaObject of metaObjectShowList">
|
||||
<div class="cols-head">
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
<div class="object-text">
|
||||
<p class="m-0 word-break mr-20">
|
||||
{{ metaObject.NAME }}
|
||||
</p>
|
||||
<p class="float-right ml-3">
|
||||
{{ metaObject.ID }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<clr-tree *ngIf="showAcc" [clrLazy]="true">
|
||||
<clr-tree-node
|
||||
*clrRecursiveFor="
|
||||
let entry of root$ | async;
|
||||
getChildren: getChildren
|
||||
"
|
||||
[clrExpandable]="true"
|
||||
}
|
||||
</div>
|
||||
<div class="clr-col-6 text-center">
|
||||
<h3>{{ assoObjectSelected }}</h3>
|
||||
@if (showTable) {
|
||||
<clr-datagrid class="datagrid-custom-footer">
|
||||
<clr-dg-column>
|
||||
TYPE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="typeFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
NAME
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="nameFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
VALUE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="valueFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let metaObjectAttribute of metaObjectAttributes"
|
||||
>
|
||||
<div [class.object-header]="!entry.count" class="full-width">
|
||||
<div>
|
||||
<clr-icon
|
||||
*ngIf="!entry.count"
|
||||
shape="rack-server"
|
||||
></clr-icon>
|
||||
<clr-icon *ngIf="entry.count" shape="block"></clr-icon>
|
||||
{{ entry.display }}
|
||||
</div>
|
||||
|
||||
<p class="float-right object-uri" *ngIf="!entry.count">
|
||||
{{ entry.URI }}
|
||||
</p>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</clr-tree>
|
||||
</ng-container>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.TYPE }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.NAME }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.VALUE }}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10, 20, 50, 100]"
|
||||
>Attributes per page</clr-dg-page-size
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} Attributes
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-col-6 text-center">
|
||||
<h3>{{ assoObjectSelected }}</h3>
|
||||
<clr-datagrid class="datagrid-custom-footer" *ngIf="showTable">
|
||||
<clr-dg-column>
|
||||
TYPE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="typeFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
NAME
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="nameFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>
|
||||
VALUE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="valueFilter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let metaObjectAttribute of metaObjectAttributes"
|
||||
>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.TYPE }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.NAME }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ metaObjectAttribute.VALUE }}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10, 20, 50, 100]"
|
||||
>Attributes per page</clr-dg-page-size
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} Attributes
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,8 @@ export interface ColumnDetail {
|
||||
LENGTH: number
|
||||
NAME: string
|
||||
TYPE: string
|
||||
VARNUM: number
|
||||
// No longer sent by viewdata.sas — optional for legacy responses.
|
||||
VARNUM?: number
|
||||
}
|
||||
|
||||
export interface Approver {
|
||||
|
||||
@@ -1,117 +1,118 @@
|
||||
<app-sidebar>
|
||||
<div *ngIf="datasetsLoading" class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!parsedDatasets.length" class="text-center mb-10">
|
||||
<button
|
||||
(click)="fileUploadInput.click()"
|
||||
id="browse-file"
|
||||
class="btn btn-primary btn-sm"
|
||||
[disabled]="selectedFile !== null || submittingCsv"
|
||||
>
|
||||
Browse file
|
||||
</button>
|
||||
<input
|
||||
hidden
|
||||
#fileUploadInput
|
||||
id="file-upload"
|
||||
type="file"
|
||||
(change)="onFileChange($event)"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="parsedDatasets.length && !submittedCsvDatasets.length">
|
||||
<div *ngIf="!excelsSubmitted" class="text-center mb-10">
|
||||
<button (click)="onDiscard()" class="btn btn-danger btn-sm mr-10">
|
||||
Discard
|
||||
</button>
|
||||
<button
|
||||
(click)="onSubmitAll()"
|
||||
id="submit-all"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Submit All
|
||||
</button>
|
||||
@if (datasetsLoading) {
|
||||
<div class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!parsedDatasets.length) {
|
||||
<div class="text-center mb-10">
|
||||
<button
|
||||
(click)="fileUploadInput.click()"
|
||||
id="browse-file"
|
||||
class="btn btn-primary btn-sm"
|
||||
[disabled]="selectedFile !== null || submittingCsv"
|
||||
>
|
||||
Browse file
|
||||
</button>
|
||||
<input
|
||||
hidden
|
||||
#fileUploadInput
|
||||
id="file-upload"
|
||||
type="file"
|
||||
(change)="onFileChange($event)"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (parsedDatasets.length && !submittedCsvDatasets.length) {
|
||||
@if (!excelsSubmitted) {
|
||||
<div class="text-center mb-10">
|
||||
<button (click)="onDiscard()" class="btn btn-danger btn-sm mr-10">
|
||||
Discard
|
||||
</button>
|
||||
<button
|
||||
(click)="onSubmitAll()"
|
||||
id="submit-all"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Submit All
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<p cds-text="caption" class="ml-10 mb-10">Found tables:</p>
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngFor="let dataset of parsedDatasets">
|
||||
<button
|
||||
(click)="onParsedDatasetClick(dataset)"
|
||||
class="clr-treenode-link whitespace-nowrap d-flex clr-align-items-center"
|
||||
[class.active]="dataset.active"
|
||||
>
|
||||
<ng-container *ngIf="dataset.submitResult">
|
||||
<cds-icon
|
||||
*ngIf="dataset.submitResult.error"
|
||||
status="danger"
|
||||
shape="exclamation-circle"
|
||||
></cds-icon>
|
||||
<cds-icon
|
||||
*ngIf="dataset.submitResult.success"
|
||||
status="success"
|
||||
shape="check-circle"
|
||||
></cds-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!dataset.submitResult">
|
||||
<ng-container *ngIf="dataset.datasource">
|
||||
<cds-icon
|
||||
*ngIf="!(dataset.datasource.length && dataset.parseResult)"
|
||||
status="danger"
|
||||
shape="exclamation-circle"
|
||||
></cds-icon>
|
||||
<cds-icon
|
||||
*ngIf="dataset.datasource.length && dataset.parseResult"
|
||||
shape="table"
|
||||
></cds-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!dataset.datasource">
|
||||
<cds-icon *ngIf="!dataset.parsingTable" shape="table"></cds-icon>
|
||||
|
||||
<clr-spinner *ngIf="dataset.parsingTable" clrSmall></clr-spinner>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<span class="ml-5"> {{ dataset.libds }} </span>
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
@for (dataset of parsedDatasets; track dataset) {
|
||||
<clr-tree-node>
|
||||
<button
|
||||
(click)="onParsedDatasetClick(dataset)"
|
||||
class="clr-treenode-link whitespace-nowrap d-flex clr-align-items-center"
|
||||
[class.active]="dataset.active"
|
||||
>
|
||||
@if (dataset.submitResult) {
|
||||
@if (dataset.submitResult.error) {
|
||||
<cds-icon status="danger" shape="exclamation-circle"></cds-icon>
|
||||
}
|
||||
@if (dataset.submitResult.success) {
|
||||
<cds-icon status="success" shape="check-circle"></cds-icon>
|
||||
}
|
||||
}
|
||||
@if (!dataset.submitResult) {
|
||||
@if (dataset.datasource) {
|
||||
@if (!(dataset.datasource.length && dataset.parseResult)) {
|
||||
<cds-icon
|
||||
status="danger"
|
||||
shape="exclamation-circle"
|
||||
></cds-icon>
|
||||
}
|
||||
@if (dataset.datasource.length && dataset.parseResult) {
|
||||
<cds-icon shape="table"></cds-icon>
|
||||
}
|
||||
}
|
||||
@if (!dataset.datasource) {
|
||||
@if (!dataset.parsingTable) {
|
||||
<cds-icon shape="table"></cds-icon>
|
||||
}
|
||||
@if (dataset.parsingTable) {
|
||||
<clr-spinner clrSmall></clr-spinner>
|
||||
}
|
||||
}
|
||||
}
|
||||
<span class="ml-5"> {{ dataset.libds }} </span>
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="submittedCsvDatasets.length">
|
||||
@if (submittedCsvDatasets.length) {
|
||||
<p cds-text="caption" class="ml-10 mb-10 mt-10">Submitted tables:</p>
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngFor="let dataset of submittedCsvDatasets">
|
||||
<button
|
||||
(click)="onSubmittedCsvDatasetClick(dataset)"
|
||||
class="clr-treenode-link whitespace-nowrap"
|
||||
[class.active]="dataset.active"
|
||||
>
|
||||
<cds-icon
|
||||
*ngIf="dataset.error"
|
||||
status="danger"
|
||||
shape="exclamation-circle"
|
||||
></cds-icon>
|
||||
<cds-icon
|
||||
*ngIf="dataset.success"
|
||||
status="success"
|
||||
shape="check-circle"
|
||||
></cds-icon>
|
||||
<cds-icon shape="table"></cds-icon>
|
||||
{{ dataset.libds }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
@for (dataset of submittedCsvDatasets; track dataset) {
|
||||
<clr-tree-node>
|
||||
<button
|
||||
(click)="onSubmittedCsvDatasetClick(dataset)"
|
||||
class="clr-treenode-link whitespace-nowrap"
|
||||
[class.active]="dataset.active"
|
||||
>
|
||||
@if (dataset.error) {
|
||||
<cds-icon status="danger" shape="exclamation-circle"></cds-icon>
|
||||
}
|
||||
@if (dataset.success) {
|
||||
<cds-icon status="success" shape="check-circle"></cds-icon>
|
||||
}
|
||||
<cds-icon shape="table"></cds-icon>
|
||||
{{ dataset.libds }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- <div *ngIf="librariesPaging" class="w-100 text-center">
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div> -->
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div> -->
|
||||
</app-sidebar>
|
||||
|
||||
<div #contentArea class="content-area">
|
||||
@@ -122,23 +123,22 @@
|
||||
<p cds-text="section">Multi Dataset Load</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="selectedFile === null && !submittingCsv"
|
||||
class="no-table-selected pointer-events-none"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="upload-cloud"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please upload a file
|
||||
</p>
|
||||
</div>
|
||||
@if (selectedFile === null && !submittingCsv) {
|
||||
<div class="no-table-selected pointer-events-none">
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="upload-cloud"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please upload a file
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="selectedFile !== null || submittingCsv">
|
||||
<ng-container *ngIf="!parsedDatasets.length && selectedFile !== null">
|
||||
@if (selectedFile !== null || submittingCsv) {
|
||||
@if (!parsedDatasets.length && selectedFile !== null) {
|
||||
<div class="d-flex clr-justify-content-center mt-15">
|
||||
<div class="dataset-input-wrapper">
|
||||
<p cds-text="secondary regular" class="mb-5">
|
||||
@@ -160,12 +160,10 @@
|
||||
<p cds-text="secondary regular" class="mb-15">
|
||||
Paste or type the list of datasets to upload:
|
||||
</p>
|
||||
|
||||
<clr-control-helper class="mb-5"
|
||||
>Each row is one dataset. We will automatically detect tables by
|
||||
the sheetname and populate if any.</clr-control-helper
|
||||
>
|
||||
|
||||
<hot-table
|
||||
#hotInstanceUserDataset
|
||||
id="hotTableUserDataset"
|
||||
@@ -173,7 +171,6 @@
|
||||
[settings]="hotUserDatasetsSettings"
|
||||
>
|
||||
</hot-table>
|
||||
|
||||
<div class="dataset-selection-actions text-right mt-10">
|
||||
<button
|
||||
(click)="onStartParsingFile()"
|
||||
@@ -187,109 +184,90 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="parsedDatasets.length && !submittedCsvDatasets.length"
|
||||
>
|
||||
<div
|
||||
*ngIf="!activeParsedDataset"
|
||||
class="no-table-selected pointer-events-none"
|
||||
>
|
||||
<ng-container *ngIf="fileLoadingState !== FileLoadingState.parsed">
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="process-on-vm"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
{{ fileLoadingState }}...
|
||||
</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="fileLoadingState === FileLoadingState.parsed">
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="warning-standard"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a dataset on the left to review the data
|
||||
</p>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="activeParsedDataset">
|
||||
<div
|
||||
*ngIf="activeParsedDataset.submitResult"
|
||||
class="d-flex clr-justify-content-between p-10 mt-15 submission-results"
|
||||
>
|
||||
<div>
|
||||
<p cds-text="secondary regular" class="mb-10">
|
||||
Submit Status:
|
||||
<span
|
||||
*ngIf="activeParsedDataset.submitResult?.success"
|
||||
class="color-green"
|
||||
><strong>SUCCESS</strong></span
|
||||
>
|
||||
<span
|
||||
*ngIf="activeParsedDataset.submitResult?.error"
|
||||
class="color-red"
|
||||
><strong>ERROR</strong></span
|
||||
>
|
||||
}
|
||||
@if (parsedDatasets.length && !submittedCsvDatasets.length) {
|
||||
@if (!activeParsedDataset) {
|
||||
<div class="no-table-selected pointer-events-none">
|
||||
@if (fileLoadingState !== FileLoadingState.parsed) {
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="process-on-vm"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
{{ fileLoadingState }}...
|
||||
</p>
|
||||
<p
|
||||
*ngIf="activeParsedDataset.submitResult?.error"
|
||||
cds-text="secondary regular"
|
||||
>
|
||||
Error details:
|
||||
}
|
||||
@if (fileLoadingState === FileLoadingState.parsed) {
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="warning-standard"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a dataset on the left to review the data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
*ngIf="
|
||||
!submittingCsv && activeParsedDataset.submitResult?.error
|
||||
"
|
||||
(click)="reSubmitTable(activeParsedDataset)"
|
||||
class="btn btn-primary mt-10"
|
||||
[clrLoading]="submitLoading"
|
||||
>
|
||||
Resubmit
|
||||
</button>
|
||||
<button
|
||||
(click)="
|
||||
downloadFile(
|
||||
activeParsedDataset.submitResult.log ||
|
||||
activeParsedDataset.submitResult.success ||
|
||||
activeParsedDataset.submitResult.error
|
||||
)
|
||||
"
|
||||
class="btn btn-primary-outline mt-10"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="activeParsedDataset.submitResult?.error"
|
||||
class="error-field mt-15"
|
||||
>
|
||||
<div class="log-wrapper">
|
||||
{{ activeParsedDataset.submitResult?.error | json }}
|
||||
}
|
||||
@if (activeParsedDataset) {
|
||||
@if (activeParsedDataset.submitResult) {
|
||||
<div
|
||||
class="d-flex clr-justify-content-between p-10 mt-15 submission-results"
|
||||
>
|
||||
<div>
|
||||
<p cds-text="secondary regular" class="mb-10">
|
||||
Submit Status:
|
||||
@if (activeParsedDataset.submitResult.success) {
|
||||
<span class="color-green"><strong>SUCCESS</strong></span>
|
||||
}
|
||||
@if (activeParsedDataset.submitResult.error) {
|
||||
<span class="color-red"><strong>ERROR</strong></span>
|
||||
}
|
||||
</p>
|
||||
@if (activeParsedDataset.submitResult.error) {
|
||||
<p cds-text="secondary regular">Error details:</p>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@if (!submittingCsv && activeParsedDataset.submitResult.error) {
|
||||
<button
|
||||
(click)="reSubmitTable(activeParsedDataset)"
|
||||
class="btn btn-primary mt-10"
|
||||
[clrLoading]="submitLoading"
|
||||
>
|
||||
Resubmit
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
(click)="
|
||||
downloadFile(
|
||||
activeParsedDataset.submitResult.log ||
|
||||
activeParsedDataset.submitResult.success ||
|
||||
activeParsedDataset.submitResult.error
|
||||
)
|
||||
"
|
||||
class="btn btn-primary-outline mt-10"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
@if (activeParsedDataset.submitResult?.error) {
|
||||
<div class="error-field mt-15">
|
||||
<div class="log-wrapper">
|
||||
{{ activeParsedDataset.submitResult?.error | json }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex clr-justify-content-between p-10 mt-15">
|
||||
<div>
|
||||
<p cds-text="secondary regular" class="mb-10">
|
||||
Found in range:
|
||||
|
||||
<ng-container *ngIf="activeParsedDataset.parseResult">
|
||||
@if (activeParsedDataset.parseResult) {
|
||||
<strong
|
||||
>"{{
|
||||
activeParsedDataset.parseResult.rangeSheetRes?.sheetName
|
||||
@@ -298,21 +276,18 @@
|
||||
?.rangeAddress
|
||||
}}</strong
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="!activeParsedDataset.parseResult">
|
||||
<strong *ngIf="!activeParsedDataset.parsingTable"
|
||||
>No data found</strong
|
||||
>
|
||||
|
||||
<span
|
||||
*ngIf="activeParsedDataset.parsingTable"
|
||||
class="d-flex clr-align-items-center"
|
||||
>
|
||||
<strong>Searching for the data...</strong>
|
||||
<clr-spinner class="ml-5" clrSmall></clr-spinner>
|
||||
</span>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!activeParsedDataset.parseResult) {
|
||||
@if (!activeParsedDataset.parsingTable) {
|
||||
<strong>No data found</strong>
|
||||
}
|
||||
@if (activeParsedDataset.parsingTable) {
|
||||
<span class="d-flex clr-align-items-center">
|
||||
<strong>Searching for the data...</strong>
|
||||
<clr-spinner class="ml-5" clrSmall></clr-spinner>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
<p cds-text="secondary regular">
|
||||
Dataset:
|
||||
@@ -333,7 +308,6 @@
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<clr-toggle-wrapper>
|
||||
<input
|
||||
@@ -354,11 +328,11 @@
|
||||
</clr-toggle-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isHotHidden" class="text-center w-100">
|
||||
<clr-spinner class="spinner-md"></clr-spinner>
|
||||
</div>
|
||||
|
||||
@if (isHotHidden) {
|
||||
<div class="text-center w-100">
|
||||
<clr-spinner class="spinner-md"></clr-spinner>
|
||||
</div>
|
||||
}
|
||||
<hot-table
|
||||
#hotInstanceMain
|
||||
id="hotTable"
|
||||
@@ -366,27 +340,24 @@
|
||||
[settings]="hotMainTableSettings"
|
||||
>
|
||||
</hot-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="submittedCsvDatasets.length">
|
||||
<div
|
||||
*ngIf="!activeSubmittedCsvDataset"
|
||||
class="no-table-selected pointer-events-none"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="warning-standard"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a dataset on the left to review the submit results
|
||||
</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="activeSubmittedCsvDataset">
|
||||
}
|
||||
}
|
||||
@if (submittedCsvDatasets.length) {
|
||||
@if (!activeSubmittedCsvDataset) {
|
||||
<div class="no-table-selected pointer-events-none">
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="warning-standard"
|
||||
size="40"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a dataset on the left to review the submit results
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (activeSubmittedCsvDataset) {
|
||||
<div class="d-flex clr-justify-content-between p-10">
|
||||
<div>
|
||||
<p cds-text="secondary regular" class="mb-10">
|
||||
@@ -409,23 +380,17 @@
|
||||
</p>
|
||||
<p cds-text="secondary regular" class="mb-10">
|
||||
Status:
|
||||
<span
|
||||
*ngIf="activeSubmittedCsvDataset.success"
|
||||
class="color-green"
|
||||
><strong>SUCCESS</strong></span
|
||||
>
|
||||
<span *ngIf="activeSubmittedCsvDataset.error" class="color-red"
|
||||
><strong>ERROR</strong></span
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="activeSubmittedCsvDataset.error"
|
||||
cds-text="secondary regular"
|
||||
>
|
||||
Error details:
|
||||
@if (activeSubmittedCsvDataset.success) {
|
||||
<span class="color-green"><strong>SUCCESS</strong></span>
|
||||
}
|
||||
@if (activeSubmittedCsvDataset.error) {
|
||||
<span class="color-red"><strong>ERROR</strong></span>
|
||||
}
|
||||
</p>
|
||||
@if (activeSubmittedCsvDataset.error) {
|
||||
<p cds-text="secondary regular">Error details:</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
(click)="
|
||||
@@ -440,28 +405,29 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="activeSubmittedCsvDataset.error" class="error-field mt-15">
|
||||
<div class="log-wrapper">
|
||||
{{ activeSubmittedCsvDataset.error | json }}
|
||||
@if (activeSubmittedCsvDataset.error) {
|
||||
<div class="error-field mt-15">
|
||||
<div class="log-wrapper">
|
||||
{{ activeSubmittedCsvDataset.error | json }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<!-- <div>
|
||||
<p
|
||||
<p
|
||||
*ngIf="
|
||||
licenceState.value.viewer_rows_allowed !== Infinity &&
|
||||
hotTable.data &&
|
||||
hotTable.data.length > licenceState.value.viewer_rows_allowed
|
||||
"
|
||||
class="mt-2-i w-100 text-center"
|
||||
>
|
||||
To display more than {{ licenceState.value.viewer_rows_allowed }} rows,
|
||||
contact <contact-link />
|
||||
</p>
|
||||
</div> -->
|
||||
class="mt-2-i w-100 text-center"
|
||||
>
|
||||
To display more than {{ licenceState.value.viewer_rows_allowed }} rows,
|
||||
contact <contact-link />
|
||||
</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -471,16 +437,14 @@
|
||||
{{ tablesToSubmit.length === 1 ? 'table' : 'tables' }} for approval
|
||||
</h3>
|
||||
<div class="modal-body">
|
||||
<p
|
||||
*ngIf="licenceState.value.submit_rows_limit !== Infinity"
|
||||
cds-text="body"
|
||||
class="licence-limit-notice mt-0 mb-15"
|
||||
>
|
||||
Due to current licence, only
|
||||
{{ licenceState.value.submit_rows_limit }} rows in each file will be
|
||||
submitted. To remove the restriction, contact
|
||||
support@datacontroller.io.
|
||||
</p>
|
||||
@if (licenceState.value.submit_rows_limit !== Infinity) {
|
||||
<p cds-text="body" class="licence-limit-notice mt-0 mb-15">
|
||||
Due to current licence, only
|
||||
{{ licenceState.value.submit_rows_limit }} rows in each file will be
|
||||
submitted. To remove the restriction, contact
|
||||
support@datacontroller.io.
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="text-area-full-width">
|
||||
<label for="formFields_8" class="mb-5 d-block">Message</label>
|
||||
|
||||
@@ -30,20 +30,21 @@
|
||||
(ngModelChange)="setGroupLogic(groupLogic)"
|
||||
clrSelect
|
||||
>
|
||||
<option
|
||||
*ngFor="let logic of logicOperators"
|
||||
[selected]="logicOperators[0]"
|
||||
>
|
||||
{{ logic }}
|
||||
</option>
|
||||
@for (logic of logicOperators; track logic) {
|
||||
<option [selected]="logicOperators[0]">
|
||||
{{ logic }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
<div class="clr-col-md-10 mb-30">
|
||||
<pre class="line-numbers language-markup">
|
||||
<div *ngIf="whereClauseLoading" class="progresStatic progress loop">
|
||||
<progress></progress>
|
||||
</div>
|
||||
@if (whereClauseLoading) {
|
||||
<div class="progresStatic progress loop">
|
||||
<progress></progress>
|
||||
</div>
|
||||
}
|
||||
|
||||
<code class="language-sql">{{whereClause}}</code>
|
||||
</pre>
|
||||
@@ -55,245 +56,246 @@
|
||||
class="clauses-container clr-col-md-12"
|
||||
[class.clr-col-md-10]="clauses?.queryObj?.length > 1"
|
||||
>
|
||||
<div *ngIf="clauses?.queryObj?.length > 1"></div>
|
||||
@if (clauses?.queryObj?.length > 1) {
|
||||
<div></div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="clause-row"
|
||||
*ngFor="let clause of clauses.queryObj; let clauseIndex = index"
|
||||
>
|
||||
<div class="clr-row" [class.invalid-clause]="clause.invalidClause">
|
||||
<div class="clause-logic clr-col-md-2">
|
||||
<div class="select">
|
||||
<clr-select-container>
|
||||
<label>Logic</label>
|
||||
|
||||
<select
|
||||
[(ngModel)]="clause.clauseLogic"
|
||||
(ngModelChange)="setLogic()"
|
||||
[disabled]="clause.elements.length < 2"
|
||||
clrSelect
|
||||
>
|
||||
<option
|
||||
*ngFor="let logic of logicOperators"
|
||||
[selected]="logicOperators[0]"
|
||||
>
|
||||
{{ logic }}
|
||||
</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button
|
||||
*ngIf="innerWidth > 768"
|
||||
class="btn btn-primary btn-block mt-10"
|
||||
(click)="addGroupClause()"
|
||||
>
|
||||
<clr-icon shape="plus"></clr-icon>
|
||||
<span>Group</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clause-query clr-col-md-10">
|
||||
<clr-icon
|
||||
*ngIf="clauses.queryObj.length > 1"
|
||||
(click)="removeGroupClause(clauseIndex)"
|
||||
shape="times"
|
||||
size="36"
|
||||
class="remove-group-clause-button"
|
||||
></clr-icon>
|
||||
|
||||
<div
|
||||
class="clr-row"
|
||||
*ngFor="let query of clause.elements; let queryIndex = index"
|
||||
[class.invalid-clause]="query.invalidClause"
|
||||
>
|
||||
<!-- VARIABLE -->
|
||||
<div class="variable-col form-group clr-col-md-3">
|
||||
<div class="datalist-wrapper">
|
||||
<app-soft-select
|
||||
label="Variable"
|
||||
[id]="'select_vals_var_id' + queryIndex + '_' + clauseIndex"
|
||||
[inputId]="'vals_var_id' + queryIndex + '_' + clauseIndex"
|
||||
[emitOnlySelected]="true"
|
||||
[(value)]="query.variable"
|
||||
(onInputEvent)="
|
||||
variableInputChange(
|
||||
query.variable,
|
||||
queryIndex,
|
||||
clauseIndex,
|
||||
$event
|
||||
)
|
||||
"
|
||||
>
|
||||
<option *ngFor="let column of cols">
|
||||
{{ column.NAME }}
|
||||
</option>
|
||||
</app-soft-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OPERATOR -->
|
||||
<div class="operator-col form-group clr-col-md-3">
|
||||
@for (clause of clauses.queryObj; track clause; let clauseIndex = $index) {
|
||||
<div class="clause-row">
|
||||
<div class="clr-row" [class.invalid-clause]="clause.invalidClause">
|
||||
<div class="clause-logic clr-col-md-2">
|
||||
<div class="select">
|
||||
<clr-select-container>
|
||||
<label>Operator</label>
|
||||
<label>Logic</label>
|
||||
<select
|
||||
[(ngModel)]="query.operator"
|
||||
(ngModelChange)="
|
||||
setVariableOperator(queryIndex, query.operator, clauseIndex)
|
||||
"
|
||||
[(ngModel)]="clause.clauseLogic"
|
||||
(ngModelChange)="setLogic()"
|
||||
[disabled]="clause.elements.length < 2"
|
||||
clrSelect
|
||||
>
|
||||
<option *ngFor="let opr of query.operators">{{ opr }}</option>
|
||||
@for (logic of logicOperators; track logic) {
|
||||
<option [selected]="logicOperators[0]">
|
||||
{{ logic }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
|
||||
<!-- VALUE -->
|
||||
<div
|
||||
*ngVar="
|
||||
query.ddtype === 'DATE' ||
|
||||
query.ddtype === 'DATETIME' ||
|
||||
query.ddtype === 'TIME' as isDateTime
|
||||
"
|
||||
class="value-col form-group clr-col-md-3"
|
||||
>
|
||||
<div
|
||||
*ngIf="query.operator === 'IN' || query.operator === 'NOT IN'"
|
||||
class="checkbox-vals"
|
||||
>
|
||||
<button
|
||||
(click)="
|
||||
currentQueryIndex = queryIndex;
|
||||
currentClauseIndex = clauseIndex
|
||||
"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Choose values
|
||||
</button>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
checkboxValues;
|
||||
context: {
|
||||
query: query,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex
|
||||
}
|
||||
"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
query.operator !== 'BETWEEN' &&
|
||||
query.operator !== 'IN' &&
|
||||
query.operator !== 'NOT IN' &&
|
||||
query.operator !== 'LIKE' &&
|
||||
query.operator !== 'CONTAINS' &&
|
||||
query.operator !== 'BEGINS_WITH'
|
||||
"
|
||||
class="single-field-vals"
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPicker;
|
||||
context: {
|
||||
query: query,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="query.operator === 'BETWEEN'" class="range-vals">
|
||||
<div class="from">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPickerRange;
|
||||
context: {
|
||||
range: 'start',
|
||||
query: query,
|
||||
queryValueIndex: 0,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="to">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPickerRange;
|
||||
context: {
|
||||
range: 'end',
|
||||
query: query,
|
||||
queryValueIndex: 1,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
query.operator === 'LIKE' ||
|
||||
query.operator === 'BEGINS_WITH' ||
|
||||
query.operator === 'CONTAINS'
|
||||
"
|
||||
class="contains-vals"
|
||||
>
|
||||
<label class="clr-control-label">Value</label>
|
||||
|
||||
<input
|
||||
class="input-val"
|
||||
type="text"
|
||||
[(ngModel)]="query.value"
|
||||
(ngModelChange)="
|
||||
setVariableValues($event, queryIndex, clauseIndex)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clause-buttons clr-col-md-2 btn-group">
|
||||
<br />
|
||||
@if (innerWidth > 768) {
|
||||
<button
|
||||
class="btn btn-warning btn-block"
|
||||
(click)="removeClause(queryIndex, clauseIndex)"
|
||||
[disabled]="clauses.queryObj[clauseIndex].elements.length === 1"
|
||||
>
|
||||
<clr-icon shape="minus"></clr-icon>
|
||||
<span></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-success btn-block"
|
||||
(click)="addClause(clauseIndex)"
|
||||
class="btn btn-primary btn-block mt-10"
|
||||
(click)="addGroupClause()"
|
||||
>
|
||||
<clr-icon shape="plus"></clr-icon>
|
||||
<span></span>
|
||||
<span>Group</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="clause-query clr-col-md-10">
|
||||
@if (clauses.queryObj.length > 1) {
|
||||
<clr-icon
|
||||
(click)="removeGroupClause(clauseIndex)"
|
||||
shape="times"
|
||||
size="36"
|
||||
class="remove-group-clause-button"
|
||||
></clr-icon>
|
||||
}
|
||||
@for (
|
||||
query of clause.elements;
|
||||
track query;
|
||||
let queryIndex = $index
|
||||
) {
|
||||
<div class="clr-row" [class.invalid-clause]="query.invalidClause">
|
||||
<!-- VARIABLE -->
|
||||
<div class="variable-col form-group clr-col-md-3">
|
||||
<div class="datalist-wrapper">
|
||||
<app-soft-select
|
||||
label="Variable"
|
||||
[id]="
|
||||
'select_vals_var_id' + queryIndex + '_' + clauseIndex
|
||||
"
|
||||
[inputId]="'vals_var_id' + queryIndex + '_' + clauseIndex"
|
||||
[emitOnlySelected]="true"
|
||||
[(value)]="query.variable"
|
||||
(onInputEvent)="
|
||||
variableInputChange(
|
||||
query.variable,
|
||||
queryIndex,
|
||||
clauseIndex,
|
||||
$event
|
||||
)
|
||||
"
|
||||
>
|
||||
@for (column of cols; track column) {
|
||||
<option>
|
||||
{{ column.NAME }}
|
||||
</option>
|
||||
}
|
||||
</app-soft-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OPERATOR -->
|
||||
<div class="operator-col form-group clr-col-md-3">
|
||||
<clr-select-container>
|
||||
<label>Operator</label>
|
||||
<select
|
||||
[(ngModel)]="query.operator"
|
||||
(ngModelChange)="
|
||||
setVariableOperator(
|
||||
queryIndex,
|
||||
query.operator,
|
||||
clauseIndex
|
||||
)
|
||||
"
|
||||
clrSelect
|
||||
>
|
||||
@for (opr of query.operators; track opr) {
|
||||
<option>{{ opr }}</option>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
<!-- VALUE -->
|
||||
<div
|
||||
*ngVar="
|
||||
query.ddtype === 'DATE' ||
|
||||
query.ddtype === 'DATETIME' ||
|
||||
query.ddtype === 'TIME' as isDateTime
|
||||
"
|
||||
class="value-col form-group clr-col-md-3"
|
||||
>
|
||||
@if (query.operator === 'IN' || query.operator === 'NOT IN') {
|
||||
<div class="checkbox-vals">
|
||||
<button
|
||||
(click)="
|
||||
currentQueryIndex = queryIndex;
|
||||
currentClauseIndex = clauseIndex
|
||||
"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Choose values
|
||||
</button>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
checkboxValues;
|
||||
context: {
|
||||
query: query,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex
|
||||
}
|
||||
"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
query.operator !== 'BETWEEN' &&
|
||||
query.operator !== 'IN' &&
|
||||
query.operator !== 'NOT IN' &&
|
||||
query.operator !== 'LIKE' &&
|
||||
query.operator !== 'CONTAINS' &&
|
||||
query.operator !== 'BEGINS_WITH'
|
||||
) {
|
||||
<div class="single-field-vals">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPicker;
|
||||
context: {
|
||||
query: query,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
}
|
||||
@if (query.operator === 'BETWEEN') {
|
||||
<div class="range-vals">
|
||||
<div class="from">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPickerRange;
|
||||
context: {
|
||||
range: 'start',
|
||||
query: query,
|
||||
queryValueIndex: 0,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="to">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
isDateTime && usePickers ? picker : notPickerRange;
|
||||
context: {
|
||||
range: 'end',
|
||||
query: query,
|
||||
queryValueIndex: 1,
|
||||
queryIndex: queryIndex,
|
||||
clauseIndex: clauseIndex,
|
||||
isDateTime: isDateTime
|
||||
}
|
||||
"
|
||||
>
|
||||
<!-- Based on check above, here correct ng-template will be put (picker or dropdown) -->
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
query.operator === 'LIKE' ||
|
||||
query.operator === 'BEGINS_WITH' ||
|
||||
query.operator === 'CONTAINS'
|
||||
) {
|
||||
<div class="contains-vals">
|
||||
<label class="clr-control-label">Value</label>
|
||||
<input
|
||||
class="input-val"
|
||||
type="text"
|
||||
[(ngModel)]="query.value"
|
||||
(ngModelChange)="
|
||||
setVariableValues($event, queryIndex, clauseIndex)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="clause-buttons clr-col-md-2 btn-group">
|
||||
<button
|
||||
class="btn btn-warning btn-block"
|
||||
(click)="removeClause(queryIndex, clauseIndex)"
|
||||
[disabled]="
|
||||
clauses.queryObj[clauseIndex].elements.length === 1
|
||||
"
|
||||
>
|
||||
<clr-icon shape="minus"></clr-icon>
|
||||
<span></span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success btn-block"
|
||||
(click)="addClause(clauseIndex)"
|
||||
>
|
||||
<clr-icon shape="plus"></clr-icon>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -305,8 +307,8 @@
|
||||
let-queryIndex="queryIndex"
|
||||
let-clauseIndex="clauseIndex"
|
||||
>
|
||||
<ng-container [ngSwitch]="query.ddtype">
|
||||
<ng-container *ngSwitchCase="'DATE'">
|
||||
@switch (query.ddtype) {
|
||||
@case ('DATE') {
|
||||
<app-soft-select
|
||||
label="Value"
|
||||
type="date"
|
||||
@@ -323,14 +325,13 @@
|
||||
>
|
||||
<!-- In case we want to enable dropdown on the pickers, uncomment below -->
|
||||
<!-- <ng-container *ngFor="let value of query.values">
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'date' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'date' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
</app-soft-select>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'DATETIME'">
|
||||
}
|
||||
@case ('DATETIME') {
|
||||
<app-soft-select
|
||||
label="Value"
|
||||
type="date"
|
||||
@@ -348,12 +349,11 @@
|
||||
>
|
||||
<!-- In case we want to enable dropdown on the pickers, uncomment below -->
|
||||
<!-- <ng-container *ngFor="let value of query.values">
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'date' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'date' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
</app-soft-select>
|
||||
|
||||
<app-soft-select
|
||||
type="time"
|
||||
[disableSoftselect]="true"
|
||||
@@ -370,14 +370,13 @@
|
||||
>
|
||||
<!-- In case we want to enable dropdown on the pickers, uncomment below -->
|
||||
<!-- <ng-container *ngFor="let value of query.values">
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'time' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'time' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
</app-soft-select>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="'TIME'">
|
||||
}
|
||||
@case ('TIME') {
|
||||
<app-soft-select
|
||||
label="Value"
|
||||
type="time"
|
||||
@@ -394,13 +393,13 @@
|
||||
>
|
||||
<!-- In case we want to enable dropdown on the pickers, uncomment below -->
|
||||
<!-- <ng-container *ngFor="let value of query.values">
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'time' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
<option *ngIf="value.unformatted !== null">
|
||||
{{ value.formatted | dateTimeFormatter: 'time' }}
|
||||
</option>
|
||||
</ng-container> -->
|
||||
</app-soft-select>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template
|
||||
@@ -425,19 +424,27 @@
|
||||
onAutocompleteLoadingMore($event, query.variable, queryIndex, clauseIndex)
|
||||
"
|
||||
>
|
||||
<div *ngIf="!query.valueVariable">
|
||||
<option [value]="column.unformatted" *ngFor="let column of query.values">
|
||||
{{ column.formatted.trim() }}
|
||||
</option>
|
||||
</div>
|
||||
@if (!query.valueVariable) {
|
||||
<div>
|
||||
@for (column of query.values; track column) {
|
||||
<option [value]="column.unformatted">
|
||||
{{ column.formatted.trim() }}
|
||||
</option>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="query.valueVariable">
|
||||
<ng-container *ngFor="let column of cols">
|
||||
<option [value]="column.NAME" *ngIf="column.TYPE === query.type">
|
||||
{{ column.NAME }}
|
||||
</option>
|
||||
</ng-container>
|
||||
</div>
|
||||
@if (query.valueVariable) {
|
||||
<div>
|
||||
@for (column of cols; track column) {
|
||||
@if (column.TYPE === query.type) {
|
||||
<option [value]="column.NAME">
|
||||
{{ column.NAME }}
|
||||
</option>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</app-soft-select>
|
||||
</ng-template>
|
||||
|
||||
@@ -466,9 +473,11 @@
|
||||
: false
|
||||
"
|
||||
>
|
||||
<option [value]="column.formatted" *ngFor="let column of query.values">
|
||||
{{ column.formatted }}
|
||||
</option>
|
||||
@for (column of query.values; track column) {
|
||||
<option [value]="column.formatted">
|
||||
{{ column.formatted }}
|
||||
</option>
|
||||
}
|
||||
</app-soft-select>
|
||||
</ng-template>
|
||||
|
||||
@@ -487,30 +496,31 @@
|
||||
>
|
||||
<h3 class="modal-title">Select values</h3>
|
||||
<div class="modal-body">
|
||||
<h5 *ngIf="!isArr(query.value)" class="no-values">
|
||||
No values available.
|
||||
</h5>
|
||||
@if (!isArr(query.value)) {
|
||||
<h5 class="no-values">No values available.</h5>
|
||||
}
|
||||
|
||||
<section class="form-block" *ngIf="isArr(query.value)">
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper
|
||||
*ngFor="let column of query.values; let i = index"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
[(ngModel)]="query.value[i].checked"
|
||||
(ngModelChange)="
|
||||
setVariableValues($event, queryIndex, clauseIndex)
|
||||
"
|
||||
/>
|
||||
|
||||
<label>
|
||||
{{ column.formatted }}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</section>
|
||||
@if (isArr(query.value)) {
|
||||
<section class="form-block">
|
||||
<clr-checkbox-container>
|
||||
@for (column of query.values; track column; let i = $index) {
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
[(ngModel)]="query.value[i].checked"
|
||||
(ngModelChange)="
|
||||
setVariableValues($event, queryIndex, clauseIndex)
|
||||
"
|
||||
/>
|
||||
<label>
|
||||
{{ column.formatted }}
|
||||
</label>
|
||||
</clr-checkbox-wrapper>
|
||||
}
|
||||
</clr-checkbox-container>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
|
||||
@@ -22,6 +22,7 @@ import { QueryDateTime } from './models/QueryDateTime.model'
|
||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||
import { get } from 'lodash-es'
|
||||
import { OnLoadingMoreEvent } from '../shared/autocomplete/autocomplete.component'
|
||||
import { getFilterObjPath } from './utils/getFilterObjPath'
|
||||
|
||||
registerLocaleData(localeEnGB)
|
||||
@Component({
|
||||
@@ -251,17 +252,7 @@ export class QueryComponent
|
||||
public setToGlobals() {
|
||||
if (!this.caching) return
|
||||
|
||||
let objPath = ''
|
||||
|
||||
if (globals.rootParam === 'home' || globals.rootParam === 'editor') {
|
||||
if (this.viewboxId) {
|
||||
objPath = `viewboxes.${this.viewboxId}`
|
||||
} else {
|
||||
objPath = 'editor'
|
||||
}
|
||||
} else if (globals.rootParam === 'view') {
|
||||
objPath = 'viewer'
|
||||
}
|
||||
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
||||
|
||||
get(globals, objPath).filter.groupLogic = this.groupLogic
|
||||
if (typeof this.whereClause === 'string') {
|
||||
@@ -280,17 +271,7 @@ export class QueryComponent
|
||||
public getFromGlobals() {
|
||||
if (!this.caching) return
|
||||
|
||||
let objPath = ''
|
||||
|
||||
if (globals.rootParam === 'home' || globals.rootParam === 'editor') {
|
||||
if (this.viewboxId) {
|
||||
objPath = `viewboxes.${this.viewboxId}`
|
||||
} else {
|
||||
objPath = 'editor'
|
||||
}
|
||||
} else if (globals.rootParam === 'view') {
|
||||
objPath = 'viewer'
|
||||
}
|
||||
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
||||
|
||||
if (get(globals, objPath).filter.cols.length > 0) {
|
||||
this.cols = JSON.parse(JSON.stringify(get(globals, objPath).filter.cols))
|
||||
@@ -1051,22 +1032,23 @@ export class QueryComponent
|
||||
this.columnsSub = this.sasStoreService.columns.subscribe(
|
||||
(response: any) => {
|
||||
let cols = response.data.cols
|
||||
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
||||
|
||||
if (globals.rootParam === 'home' || globals.rootParam === 'editor') {
|
||||
this.cols = cols
|
||||
let some = cols[0].NAME
|
||||
this.libds = response.libds
|
||||
|
||||
globals.editor.filter.cols = JSON.parse(JSON.stringify(cols))
|
||||
get(globals, objPath).filter.cols = JSON.parse(JSON.stringify(cols))
|
||||
}
|
||||
|
||||
if (globals.rootParam === 'view') {
|
||||
if (globals.viewer.filter.cols.length < 1) {
|
||||
if (get(globals, objPath).filter.cols.length < 1) {
|
||||
this.cols = cols
|
||||
let some = cols[0].NAME
|
||||
this.libds = response.libds
|
||||
|
||||
globals.viewer.filter.cols = JSON.parse(JSON.stringify(cols))
|
||||
get(globals, objPath).filter.cols = JSON.parse(JSON.stringify(cols))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,20 +1059,12 @@ export class QueryComponent
|
||||
)
|
||||
|
||||
this.valuesSub = this.sasStoreService.values.subscribe((res: any) => {
|
||||
if (globals.rootParam === 'home' || globals.rootParam === 'editor') {
|
||||
if (globals.editor.filter.vals.length < 1) {
|
||||
this.vals = res.vals
|
||||
const objPath = getFilterObjPath(globals.rootParam, this.viewboxId)
|
||||
|
||||
globals.editor.filter.vals = JSON.parse(JSON.stringify(res.vals))
|
||||
}
|
||||
}
|
||||
if (get(globals, objPath).filter.vals.length < 1) {
|
||||
this.vals = res.vals
|
||||
|
||||
if (globals.rootParam === 'view') {
|
||||
if (globals.viewer.filter.vals.length < 1) {
|
||||
this.vals = res.vals
|
||||
|
||||
globals.viewer.filter.vals = JSON.parse(JSON.stringify(res.vals))
|
||||
}
|
||||
get(globals, objPath).filter.vals = JSON.parse(JSON.stringify(res.vals))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getFilterObjPath } from './getFilterObjPath'
|
||||
|
||||
describe('getFilterObjPath', () => {
|
||||
it("resolves to 'viewer' for the view page with no viewboxId (existing correct case)", () => {
|
||||
expect(getFilterObjPath('view', undefined)).toEqual('viewer')
|
||||
})
|
||||
|
||||
it("resolves to 'editor' for the editor page with no viewboxId", () => {
|
||||
expect(getFilterObjPath('editor', undefined)).toEqual('editor')
|
||||
})
|
||||
|
||||
it("resolves to 'editor' for the home page with no viewboxId (existing home/editor grouping)", () => {
|
||||
expect(getFilterObjPath('home', undefined)).toEqual('editor')
|
||||
})
|
||||
|
||||
it("resolves to 'viewboxes.<id>' when viewboxId is set on the view page - the actual bug (previously returned 'viewer')", () => {
|
||||
expect(getFilterObjPath('view', 42)).toEqual('viewboxes.42')
|
||||
})
|
||||
|
||||
it("resolves to 'viewboxes.<id>' when viewboxId is set on the editor page (already correct today)", () => {
|
||||
expect(getFilterObjPath('editor', 42)).toEqual('viewboxes.42')
|
||||
})
|
||||
|
||||
it("resolves to 'viewboxes.<id>' when viewboxId is set on the home page (already correct today)", () => {
|
||||
expect(getFilterObjPath('home', 42)).toEqual('viewboxes.42')
|
||||
})
|
||||
|
||||
it('resolves to an empty string for an unrecognized rootParam (matches existing fallthrough)', () => {
|
||||
expect(getFilterObjPath('', undefined)).toEqual('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Resolves which bucket of the `globals` filter cache (_globals.ts) a
|
||||
* QueryComponent instance should read/write. viewboxId is checked first,
|
||||
* unconditionally.
|
||||
*/
|
||||
export const getFilterObjPath = (
|
||||
rootParam: string,
|
||||
viewboxId?: number
|
||||
): string => {
|
||||
if (viewboxId) return `viewboxes.${viewboxId}`
|
||||
if (rootParam === 'home' || rootParam === 'editor') return 'editor'
|
||||
if (rootParam === 'view') return 'viewer'
|
||||
|
||||
return ''
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,148 +1,161 @@
|
||||
<div class="content-area">
|
||||
<div class="card">
|
||||
<div *ngIf="remained === 0" class="d-flex justify-content-center">
|
||||
<div class="card-block noapprovals-info-wrapper">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<h3 class="text-center color-gray">There are no approvals remaining</h3>
|
||||
@if (remained === 0) {
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="card-block noapprovals-info-wrapper">
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info icon-dc-fill"
|
||||
></clr-icon>
|
||||
<h3 class="text-center color-gray">
|
||||
There are no approvals remaining
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="card-header" [ngClass]="{ noBorder: !loaded }">
|
||||
<h3
|
||||
class="center clr-col-md-12 text-center"
|
||||
*ngIf="loaded && remained !== 0"
|
||||
>
|
||||
REVIEW
|
||||
</h3>
|
||||
<p
|
||||
class="text-center font-weight-700 color-dark-gray"
|
||||
*ngIf="loaded && remained !== 0"
|
||||
>
|
||||
You have <span>{{ remained }} </span>approvals remaining
|
||||
</p>
|
||||
@if (loaded && remained !== 0) {
|
||||
<h3 class="center clr-col-md-12 text-center">REVIEW</h3>
|
||||
}
|
||||
@if (loaded && remained !== 0) {
|
||||
<p class="text-center font-weight-700 color-dark-gray">
|
||||
You have <span>{{ remained }} </span>approvals remaining
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="!loaded" class="approvals-list-wrapper">
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading approvals list</h3>
|
||||
@if (!loaded) {
|
||||
<div class="approvals-list-wrapper">
|
||||
@if (!loaded) {
|
||||
<span class="spinner"> Loading... </span>
|
||||
}
|
||||
@if (!loaded) {
|
||||
<div>
|
||||
<h3>Loading approvals list</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="clr-col-md-12" ng-if="loaded">
|
||||
<div *ngIf="approveList && remained !== 0">
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column [clrDgField]="'submitter'">
|
||||
SUBMITTER
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitterFilter"
|
||||
aria-label="Filter submitter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'baseTable'">
|
||||
BASE TABLE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="baseTableFilter"
|
||||
aria-label="Filter base table"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>ACTION</clr-dg-column>
|
||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||
|
||||
<clr-dg-row
|
||||
*clrDgItems="let approveItem of approveList; let i = index"
|
||||
>
|
||||
<clr-dg-cell>{{ approveItem.submitter }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.baseTable }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitted }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="clr-row d-flex justify-content-around"
|
||||
role="toolbar"
|
||||
aria-label="Table actions"
|
||||
>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
||||
(click)="getClicked(i)"
|
||||
@if (approveList && remained !== 0) {
|
||||
<div>
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column [clrDgField]="'submitter'">
|
||||
SUBMITTER
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitterFilter"
|
||||
aria-label="Filter submitter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'baseTable'">
|
||||
BASE TABLE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="baseTableFilter"
|
||||
aria-label="Filter base table"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>ACTION</clr-dg-column>
|
||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let approveItem of approveList; let i = index"
|
||||
>
|
||||
<clr-dg-cell>{{ approveItem.submitter }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.baseTable }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitted }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ approveItem.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="clr-row d-flex justify-content-around"
|
||||
role="toolbar"
|
||||
aria-label="Table actions"
|
||||
>
|
||||
<clr-icon
|
||||
shape="check"
|
||||
size="24"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Go to review page screen</span>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-red"
|
||||
(click)="!approveItem.rejectLoading ? rejecting(i) : ''"
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-green"
|
||||
(click)="getClicked(i)"
|
||||
>
|
||||
<clr-icon
|
||||
shape="check"
|
||||
size="24"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content"
|
||||
>Go to review page screen</span
|
||||
>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-red"
|
||||
(click)="!approveItem.rejectLoading ? rejecting(i) : ''"
|
||||
>
|
||||
@if (!approveItem.rejectLoading) {
|
||||
<clr-icon
|
||||
shape="ban"
|
||||
size="22"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
}
|
||||
@if (approveItem.rejectLoading) {
|
||||
<clr-spinner
|
||||
[clrSmall]="true"
|
||||
aria-hidden="true"
|
||||
></clr-spinner>
|
||||
}
|
||||
<span class="tooltip-content">Reject</span>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="getTable(approveItem.tableId)"
|
||||
>
|
||||
<clr-icon
|
||||
shape="code"
|
||||
size="28"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content"
|
||||
>Go to staged data screen</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="p-0 d-flex justify-content-center">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
aria-label="Download audit file"
|
||||
[id]="approveItem.tableId"
|
||||
(click)="
|
||||
download(approveItem.tableId); $event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="!approveItem.rejectLoading"
|
||||
shape="ban"
|
||||
size="22"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<clr-spinner
|
||||
*ngIf="approveItem.rejectLoading"
|
||||
[clrSmall]="true"
|
||||
aria-hidden="true"
|
||||
></clr-spinner>
|
||||
<span class="tooltip-content">Reject</span>
|
||||
</a>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="getTable(approveItem.tableId)"
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[3, 5, 10, 15]"
|
||||
>Items per page</clr-dg-page-size
|
||||
>
|
||||
<clr-icon
|
||||
shape="code"
|
||||
size="28"
|
||||
aria-hidden="true"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Go to staged data screen</span>
|
||||
</a>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="p-0 d-flex justify-content-center">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
aria-label="Download audit file"
|
||||
[id]="approveItem.tableId"
|
||||
(click)="
|
||||
download(approveItem.tableId); $event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[3, 5, 10, 15]"
|
||||
>Items per page</clr-dg-page-size
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} approvals
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} approvals
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<div class="content-area">
|
||||
<div
|
||||
*ngIf="noData"
|
||||
id="noDataContainer"
|
||||
class="card-block d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<clr-icon shape="warning-standard" size="60" class="is-info"></clr-icon>
|
||||
<h3 class="text-center color-gray">There is no history to show</h3>
|
||||
</div>
|
||||
@if (noData) {
|
||||
<div
|
||||
id="noDataContainer"
|
||||
class="card-block d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<clr-icon shape="warning-standard" size="60" class="is-info"></clr-icon>
|
||||
<h3 class="text-center color-gray">There is no history to show</h3>
|
||||
</div>
|
||||
}
|
||||
<clr-modal [(clrModalOpen)]="openModal" [clrModalSize]="'xl'">
|
||||
<h4 class="modal-title">Approval details</h4>
|
||||
<div class="modal-body">
|
||||
@@ -18,31 +19,38 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let col of tableTitles; let ind = index">
|
||||
<td class="left">{{ col }}</td>
|
||||
<td class="left">
|
||||
<a
|
||||
*ngIf="ind < 1"
|
||||
(click)="getTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>{{ approveData[col] }}</a
|
||||
>
|
||||
<div *ngIf="ind < 2 && ind >= 1">
|
||||
<a
|
||||
(click)="getBaseTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>VIEW</a
|
||||
>
|
||||
<span> / </span>
|
||||
<a
|
||||
(click)="getEditTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>EDIT</a
|
||||
>
|
||||
</div>
|
||||
<span *ngIf="ind >= 2">{{ approveData[col] }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@for (col of tableTitles; track col; let ind = $index) {
|
||||
<tr>
|
||||
<td class="left">{{ col }}</td>
|
||||
<td class="left">
|
||||
@if (ind < 1) {
|
||||
<a
|
||||
(click)="getTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>{{ approveData[col] }}</a
|
||||
>
|
||||
}
|
||||
@if (ind < 2 && ind >= 1) {
|
||||
<div>
|
||||
<a
|
||||
(click)="getBaseTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>VIEW</a
|
||||
>
|
||||
<span> / </span>
|
||||
<a
|
||||
(click)="getEditTable(approveData[col])"
|
||||
class="cursor-pointer table-link"
|
||||
>EDIT</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
@if (ind >= 2) {
|
||||
<span>{{ approveData[col] }}</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -58,148 +66,159 @@
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<div
|
||||
*ngIf="!loaded"
|
||||
class="h-70vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading history</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!noData && loaded" class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="center clr-col-md-12 text-center" *ngIf="loaded">HISTORY</h3>
|
||||
<p
|
||||
class="text-center font-weight-700 color-dark-gray"
|
||||
*ngIf="licenceState.value.history_rows_allowed !== Infinity"
|
||||
>
|
||||
To unlock more than
|
||||
{{ licenceState.value.history_rows_allowed }} records, contact
|
||||
support@datacontroller.io
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<clr-datagrid
|
||||
class="datagrid-history datagrid-custom-footer"
|
||||
*ngIf="loaded"
|
||||
>
|
||||
<clr-dg-column [clrDgField]="'basetable'">
|
||||
BASE_TABLE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="baseTableFilter"
|
||||
aria-label="Filter base table"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'status'">
|
||||
STATUS
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="statusFilter"
|
||||
aria-label="Filter status"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitter'">
|
||||
SUBMITTER
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitterFilter"
|
||||
aria-label="Filter submitter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submittedReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'reviewed'">
|
||||
APPROVED / REJECTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="reviewedFilter"
|
||||
aria-label="Filter reviewed date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||
|
||||
<clr-dg-row
|
||||
*clrDgItems="let historyItem of history"
|
||||
(click)="getApprIndex(historyItem)"
|
||||
>
|
||||
<clr-dg-cell class="verCenter">
|
||||
<a
|
||||
class="btn btn-sm btn-link m-0"
|
||||
(click)="getBaseTable(historyItem.basetable)"
|
||||
>{{ historyItem.basetable }}</a
|
||||
>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell
|
||||
class="verCenter"
|
||||
[ngClass]="{
|
||||
rejected: historyItem.status === 'REJECTED',
|
||||
accepted: historyItem.status === 'APPROVED'
|
||||
}"
|
||||
>{{ historyItem.status }}</clr-dg-cell
|
||||
>
|
||||
<clr-dg-cell class="verCenter">{{ historyItem.submitter }}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{
|
||||
historyItem.submittedReason
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{ historyItem.submitted }}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{ historyItem.reviewed }}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter p-0 d-flex justify-content-center">
|
||||
<button
|
||||
aria-label="Download audit file"
|
||||
class="btn btn-success"
|
||||
(click)="download(historyItem.tableId); $event.stopPropagation()"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<!-- Let's keep this part if in future we decide to do the paging instead of `load more` approach -->
|
||||
<!-- <clr-dg-footer class="d-flex justify-content-start">
|
||||
<span>items per page</span>
|
||||
|
||||
<select class="mx-5" [(ngModel)]="itemsNum">
|
||||
<option [ngValue]="3">3</option>
|
||||
<option [ngValue]="5">5</option>
|
||||
<option [ngValue]="10">10</option>
|
||||
<option [ngValue]="15">15</option>
|
||||
</select>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[clrDgPageSize]="itemsNum"
|
||||
class="center"
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} updates
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer> -->
|
||||
</clr-datagrid>
|
||||
|
||||
@if (!loaded) {
|
||||
<div
|
||||
class="load-more d-flex clr-justify-content-center clr-align-items-center"
|
||||
class="h-70vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
*ngIf="
|
||||
@if (!loaded) {
|
||||
<span class="spinner"> Loading... </span>
|
||||
}
|
||||
@if (!loaded) {
|
||||
<div>
|
||||
<h3>Loading history</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!noData && loaded) {
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
@if (loaded) {
|
||||
<h3 class="center clr-col-md-12 text-center">HISTORY</h3>
|
||||
}
|
||||
@if (licenceState.value.history_rows_allowed !== Infinity) {
|
||||
<p class="text-center font-weight-700 color-dark-gray">
|
||||
To unlock more than
|
||||
{{ licenceState.value.history_rows_allowed }} records, contact
|
||||
support@datacontroller.io
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
@if (loaded) {
|
||||
<clr-datagrid class="datagrid-history datagrid-custom-footer">
|
||||
<clr-dg-column [clrDgField]="'basetable'">
|
||||
BASE_TABLE
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="baseTableFilter"
|
||||
aria-label="Filter base table"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'status'">
|
||||
STATUS
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="statusFilter"
|
||||
aria-label="Filter status"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitter'">
|
||||
SUBMITTER
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitterFilter"
|
||||
aria-label="Filter submitter"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submittedReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'reviewed'">
|
||||
APPROVED / REJECTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="reviewedFilter"
|
||||
aria-label="Filter reviewed date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column>DOWNLOAD</clr-dg-column>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let historyItem of history"
|
||||
(click)="getApprIndex(historyItem)"
|
||||
>
|
||||
<clr-dg-cell class="verCenter">
|
||||
<a
|
||||
class="btn btn-sm btn-link m-0"
|
||||
(click)="getBaseTable(historyItem.basetable)"
|
||||
>{{ historyItem.basetable }}</a
|
||||
>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell
|
||||
class="verCenter"
|
||||
[ngClass]="{
|
||||
rejected: historyItem.status === 'REJECTED',
|
||||
accepted: historyItem.status === 'APPROVED'
|
||||
}"
|
||||
>{{ historyItem.status }}</clr-dg-cell
|
||||
>
|
||||
<clr-dg-cell class="verCenter">{{
|
||||
historyItem.submitter
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{
|
||||
historyItem.submittedReason
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{
|
||||
historyItem.submitted
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter">{{
|
||||
historyItem.reviewed
|
||||
}}</clr-dg-cell>
|
||||
<clr-dg-cell class="verCenter p-0 d-flex justify-content-center">
|
||||
<button
|
||||
aria-label="Download audit file"
|
||||
class="btn btn-success"
|
||||
(click)="
|
||||
download(historyItem.tableId); $event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<!-- Let's keep this part if in future we decide to do the paging instead of `load more` approach -->
|
||||
<!-- <clr-dg-footer class="d-flex justify-content-start">
|
||||
<span>items per page</span>
|
||||
<select class="mx-5" [(ngModel)]="itemsNum">
|
||||
<option [ngValue]="3">3</option>
|
||||
<option [ngValue]="5">5</option>
|
||||
<option [ngValue]="10">10</option>
|
||||
<option [ngValue]="15">15</option>
|
||||
</select>
|
||||
<clr-dg-pagination
|
||||
#pagination
|
||||
[clrDgPageSize]="itemsNum"
|
||||
class="center"
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} updates
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer> -->
|
||||
</clr-datagrid>
|
||||
}
|
||||
<div
|
||||
class="load-more d-flex clr-justify-content-center clr-align-items-center"
|
||||
>
|
||||
@if (
|
||||
this.licenceState.value.history_rows_allowed === Infinity &&
|
||||
rowsLeftToLoad > 0
|
||||
"
|
||||
(click)="loadData()"
|
||||
[clrLoading]="loadingMore"
|
||||
class="btn btn-success"
|
||||
>
|
||||
Load {{ rowsLeftToLoad }} more
|
||||
</button>
|
||||
) {
|
||||
<button
|
||||
(click)="loadData()"
|
||||
[clrLoading]="loadingMore"
|
||||
class="btn btn-success"
|
||||
>
|
||||
Load {{ rowsLeftToLoad }} more
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,123 +1,136 @@
|
||||
<div class="w-100">
|
||||
<div *ngIf="!subReady" class="content-area">
|
||||
<div *ngIf="!subReady" class="card">
|
||||
<div
|
||||
*ngIf="remained === 0 && loaded"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="no-submitted-tables card-block d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info"
|
||||
></clr-icon>
|
||||
<h3 class="text-center color-gray">
|
||||
You have not submitted any tables
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header" [ngClass]="{ noBorder: !loaded }">
|
||||
<h3 class="center clr-col-md-12 text-center" *ngIf="remained !== 0">
|
||||
SUBMIT QUEUE
|
||||
</h3>
|
||||
<p
|
||||
class="text-center font-weight-700 color-dark-gray"
|
||||
*ngIf="loaded && remained !== 0"
|
||||
>
|
||||
You have <span>{{ remained }} </span>submissions waiting to be
|
||||
approved
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="!loaded"
|
||||
class="h-70vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading submitted list</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-md-12" *ngIf="loaded">
|
||||
<div *ngIf="submitterList && remained !== 0">
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column>BASE TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="d-flex justify-content-center"
|
||||
>ACTION</clr-dg-column
|
||||
@if (!subReady) {
|
||||
<div class="content-area">
|
||||
@if (!subReady) {
|
||||
<div class="card">
|
||||
@if (remained === 0 && loaded) {
|
||||
<div class="d-flex justify-content-center">
|
||||
<div
|
||||
class="no-submitted-tables card-block d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<clr-icon
|
||||
shape="warning-standard"
|
||||
size="60"
|
||||
class="is-info"
|
||||
></clr-icon>
|
||||
<h3 class="text-center color-gray">
|
||||
You have not submitted any tables
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="card-header" [ngClass]="{ noBorder: !loaded }">
|
||||
@if (remained !== 0) {
|
||||
<h3 class="center clr-col-md-12 text-center">SUBMIT QUEUE</h3>
|
||||
}
|
||||
@if (loaded && remained !== 0) {
|
||||
<p class="text-center font-weight-700 color-dark-gray">
|
||||
You have <span>{{ remained }} </span>submissions waiting to be
|
||||
approved
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
@if (!loaded) {
|
||||
<div
|
||||
class="h-70vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<clr-dg-column class="d-flex justify-content-center"
|
||||
>DOWNLOAD</clr-dg-column
|
||||
>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let sub of submitterList; let i = index"
|
||||
(click)="goToDetails(sub.tableId)"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<clr-dg-cell>{{ sub.base }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ sub.submitted }}</clr-dg-cell>
|
||||
<!-- <clr-dg-cell>{{sub.approver}}</clr-dg-cell> -->
|
||||
<clr-dg-cell>{{ sub.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="row justify-content-around"
|
||||
role="tooltip"
|
||||
aria-label="Go to staged data screen"
|
||||
>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="goToStage(sub.tableId)"
|
||||
>
|
||||
<clr-icon shape="code" size="28"></clr-icon>
|
||||
<span class="tooltip-content"
|
||||
>Go to staged data screen</span
|
||||
>
|
||||
</a>
|
||||
@if (!loaded) {
|
||||
<span class="spinner"> Loading... </span>
|
||||
}
|
||||
@if (!loaded) {
|
||||
<div>
|
||||
<h3>Loading submitted list</h3>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="p-0 d-flex justify-content-center">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
aria-label="Download audit file for table record"
|
||||
(click)="download(sub.tableId); $event.stopPropagation()"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[3, 5, 10, 15]"
|
||||
>Items per page</clr-dg-page-size
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} submissions
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (loaded) {
|
||||
<div class="clr-col-md-12">
|
||||
@if (submitterList && remained !== 0) {
|
||||
<div>
|
||||
<clr-datagrid class="datagrid-compact datagrid-custom-footer">
|
||||
<clr-dg-column>BASE TABLE</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitted'">
|
||||
SUBMITTED
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submittedFilter"
|
||||
aria-label="Filter submitted date"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'submitReason'">
|
||||
SUBMIT REASON
|
||||
<clr-dg-string-filter
|
||||
[clrDgStringFilter]="submitReasonFilter"
|
||||
aria-label="Filter submit reason"
|
||||
></clr-dg-string-filter>
|
||||
</clr-dg-column>
|
||||
<clr-dg-column class="d-flex justify-content-center"
|
||||
>ACTION</clr-dg-column
|
||||
>
|
||||
<clr-dg-column class="d-flex justify-content-center"
|
||||
>DOWNLOAD</clr-dg-column
|
||||
>
|
||||
<clr-dg-row
|
||||
*clrDgItems="let sub of submitterList; let i = index"
|
||||
(click)="goToDetails(sub.tableId)"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<clr-dg-cell>{{ sub.base }}</clr-dg-cell>
|
||||
<clr-dg-cell>{{ sub.submitted }}</clr-dg-cell>
|
||||
<!-- <clr-dg-cell>{{sub.approver}}</clr-dg-cell> -->
|
||||
<clr-dg-cell>{{ sub.submitReason }}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<div
|
||||
class="row justify-content-around"
|
||||
role="tooltip"
|
||||
aria-label="Go to staged data screen"
|
||||
>
|
||||
<a
|
||||
class="column-center links tooltip tooltip-md tooltip-bottom-left color-blue"
|
||||
(click)="goToStage(sub.tableId)"
|
||||
>
|
||||
<clr-icon shape="code" size="28"></clr-icon>
|
||||
<span class="tooltip-content"
|
||||
>Go to staged data screen</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell class="p-0 d-flex justify-content-center">
|
||||
<button
|
||||
class="btn btn-success"
|
||||
aria-label="Download audit file for table record"
|
||||
(click)="
|
||||
download(sub.tableId); $event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<clr-icon shape="download"></clr-icon>
|
||||
</button>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[3, 5, 10, 15]"
|
||||
>Items per page</clr-dg-page-size
|
||||
>
|
||||
{{ pagination.firstItem + 1 }} -
|
||||
{{ pagination.lastItem + 1 }} of
|
||||
{{ pagination.totalItems }} submissions
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="subReady">
|
||||
<app-approve-details></app-approve-details>
|
||||
</div>
|
||||
@if (subReady) {
|
||||
<div>
|
||||
<app-approve-details></app-approve-details>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,130 +1,146 @@
|
||||
<app-sidebar class="sidebar-height">
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="roles" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Roles"
|
||||
name="input"
|
||||
[(ngModel)]="roleSearch"
|
||||
(keyup)="roleListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="roleSearch = ''; roleListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
<ng-container *ngFor="let role of roles">
|
||||
<clr-tree-node
|
||||
(click)="roleOnClick(role)"
|
||||
*ngIf="!role['hidden']"
|
||||
[class.active]="role.ROLEURI === roleUri"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="blocks-group"></clr-icon>
|
||||
{{ role.ROLENAME }}
|
||||
</p>
|
||||
@if (roles) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Roles"
|
||||
name="input"
|
||||
[(ngModel)]="roleSearch"
|
||||
(keyup)="roleListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="roleSearch = ''; roleListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
@for (role of roles; track role) {
|
||||
@if (!role['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="roleOnClick(role)"
|
||||
[class.active]="role.ROLEURI === roleUri"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="blocks-group"></clr-icon>
|
||||
{{ role.ROLENAME }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
<div class="content-area">
|
||||
<div *ngIf="loading" class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
<div *ngIf="roleMembers && !loading">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table role-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="role-info-text">
|
||||
<b>{{ roleName }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<i>{{ roleDesc }}</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (loading) {
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card role-data">
|
||||
<div>
|
||||
<h3>MEMBERS ({{ roleMembersCount }})</h3>
|
||||
<h5 *ngIf="roleMembersCount == 0">No Members Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="roleMembersCount != 0" class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
[routerLink]="'/view/usernav/users/' + member.URIMEM"
|
||||
*ngFor="let member of roleMembers"
|
||||
>
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
<td class="">{{ member.EMAIL }}</td>
|
||||
<td class="">{{ member.MEMBERCREATED }}</td>
|
||||
<td class="">{{ member.MEMBERUPDATED }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
@if (roleMembers && !loading) {
|
||||
<div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table role-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="role-info-text">
|
||||
<b>{{ roleName }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<i>{{ roleDesc }}</i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card role-data">
|
||||
<div>
|
||||
<h3>MEMBERS ({{ roleMembersCount }})</h3>
|
||||
@if (roleMembersCount == 0) {
|
||||
<h5>No Members Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (roleMembersCount != 0) {
|
||||
<table class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (member of roleMembers; track member) {
|
||||
<tr
|
||||
[routerLink]="'/view/usernav/users/' + member.URIMEM"
|
||||
>
|
||||
<td class="">{{ member.MEMBERNAME }}</td>
|
||||
<td class="">{{ member.EMAIL }}</td>
|
||||
<td class="">{{ member.MEMBERCREATED }}</td>
|
||||
<td class="">{{ member.MEMBERUPDATED }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<h3>Groups ({{ roleGroupsCount }})</h3>
|
||||
<h5 *ngIf="roleGroupsCount == 0">No Groups Present !</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="roleGroupsCount != 0" class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
[routerLink]="'/view/usernav/groups/' + group.URIMEM"
|
||||
*ngFor="let group of roleGroups"
|
||||
>
|
||||
<td class="">{{ group.MEMBERNAME }}</td>
|
||||
<td class="">{{ group.EMAIL }}</td>
|
||||
<td class="">{{ group.MEMBERCREATED }}</td>
|
||||
<td class="">{{ group.MEMBERUPDATED }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<div>
|
||||
<h3>Groups ({{ roleGroupsCount }})</h3>
|
||||
@if (roleGroupsCount == 0) {
|
||||
<h5>No Groups Present !</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (roleGroupsCount != 0) {
|
||||
<table class="table member-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-25"><b>NAME</b></td>
|
||||
<td class="width-25"><b>EMAIL</b></td>
|
||||
<td class="width-25"><b>CREATED</b></td>
|
||||
<td class=""><b>UPDATED</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (group of roleGroups; track group) {
|
||||
<tr
|
||||
[routerLink]="'/view/usernav/groups/' + group.URIMEM"
|
||||
>
|
||||
<td class="">{{ group.MEMBERNAME }}</td>
|
||||
<td class="">{{ group.EMAIL }}</td>
|
||||
<td class="">{{ group.MEMBERCREATED }}</td>
|
||||
<td class="">{{ group.MEMBERUPDATED }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -55,8 +55,8 @@ describe('VaFilterService', () => {
|
||||
'SOME_DATETIME'
|
||||
]
|
||||
const cols = [
|
||||
{ NAME: 'SOME_CHAR', DDTYPE: 'CHARACTER' },
|
||||
{ NAME: 'SOME_NUM', DDTYPE: 'NUMERIC' },
|
||||
{ NAME: 'SOME_CHAR', DDTYPE: 'C' },
|
||||
{ NAME: 'SOME_NUM', DDTYPE: 'N' },
|
||||
{ NAME: 'SOME_TIME', DDTYPE: 'TIME' },
|
||||
{ NAME: 'SOME_DATE', DDTYPE: 'DATE' },
|
||||
{ NAME: 'SOME_DATETIME', DDTYPE: 'DATETIME' }
|
||||
|
||||
@@ -163,14 +163,20 @@ export class VaFilterService {
|
||||
|
||||
/**
|
||||
* SAS data-type kind of a column spec. DDTYPE carries
|
||||
* TIME/DATE/DATETIME/NUMERIC/CHARACTER; checked DATETIME-before-DATE (substring).
|
||||
* TIME/DATE/DATETIME in full, with C/N for CHARACTER/NUMERIC (legacy
|
||||
* responses may still carry the full strings); checked DATETIME-before-DATE
|
||||
* (substring).
|
||||
*/
|
||||
private columnKind(col: any): VaColumnKind {
|
||||
const ddtype = (col?.DDTYPE ?? '').toString().toUpperCase()
|
||||
if (ddtype.includes('DATETIME')) return 'datetime'
|
||||
if (ddtype.includes('DATE')) return 'date'
|
||||
if (ddtype.includes('TIME')) return 'time'
|
||||
if (ddtype.includes('NUMERIC') || (col?.TYPE ?? '') === 'num') {
|
||||
if (
|
||||
ddtype === 'N' ||
|
||||
ddtype.includes('NUMERIC') ||
|
||||
(col?.TYPE ?? '') === 'num'
|
||||
) {
|
||||
return 'numeric'
|
||||
}
|
||||
return 'char'
|
||||
|
||||
@@ -7,66 +7,62 @@
|
||||
>
|
||||
<h3 class="modal-title">
|
||||
{{ data.modalTitle }}
|
||||
<p
|
||||
*ngIf="data.sasService && data.sasService.length > 0"
|
||||
class="sasService mt-0"
|
||||
>
|
||||
SAS Service: <strong>{{ data.sasService }}</strong>
|
||||
</p>
|
||||
@if (data.sasService && data.sasService.length > 0) {
|
||||
<p class="sasService mt-0">
|
||||
SAS Service: <strong>{{ data.sasService }}</strong>
|
||||
</p>
|
||||
}
|
||||
</h3>
|
||||
<div class="modal-body">
|
||||
<div [innerHTML]="data.message" class="abortMsg"></div>
|
||||
|
||||
<div *ngIf="data.details !== null" class="systext">
|
||||
<p><strong>SYSWARNINGTEXT:</strong> {{ data.details.SYSWARNINGTEXT }}</p>
|
||||
<p><strong>SYSERRORTEXT:</strong> {{ data.details.SYSERRORTEXT }}</p>
|
||||
<p><strong>MAC:</strong> {{ data.details.MAC }}</p>
|
||||
</div>
|
||||
@if (data.details !== null) {
|
||||
<div class="systext">
|
||||
<p>
|
||||
<strong>SYSWARNINGTEXT:</strong> {{ data.details.SYSWARNINGTEXT }}
|
||||
</p>
|
||||
<p><strong>SYSERRORTEXT:</strong> {{ data.details.SYSERRORTEXT }}</p>
|
||||
<p><strong>MAC:</strong> {{ data.details.MAC }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
*ngIf="showConfiguratorButton(data.sasService)"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="openConfigurator()"
|
||||
>
|
||||
Open configurator
|
||||
</button>
|
||||
@if (showConfiguratorButton(data.sasService)) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="openConfigurator()"
|
||||
>
|
||||
Open configurator
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
*ngIf="data.details !== null"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="openRequestsModal()"
|
||||
>
|
||||
Open requests modal
|
||||
</button>
|
||||
@if (data.details !== null) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="openRequestsModal()"
|
||||
>
|
||||
Open requests modal
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
*ngIf="data.details?.LOG && (data.details?.LOG?.trim())!.length > 0"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="downloadLog()"
|
||||
>
|
||||
Download log
|
||||
</button>
|
||||
@if (data.details?.LOG && (data.details?.LOG?.trim())!.length > 0) {
|
||||
<button type="button" class="btn btn-primary" (click)="downloadLog()">
|
||||
Download log
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
*ngIf="!forceReload"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="closeAbortModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@if (!forceReload) {
|
||||
<button type="button" class="btn btn-primary" (click)="closeAbortModal()">
|
||||
Close
|
||||
</button>
|
||||
}
|
||||
|
||||
<button
|
||||
*ngIf="forceReload"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="reload()"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
@if (forceReload) {
|
||||
<button type="button" class="btn btn-primary" (click)="reload()">
|
||||
Reload
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<clr-alerts *ngIf="hasOpenAlert">
|
||||
<clr-alert
|
||||
*ngFor="let alert of alerts"
|
||||
[clrAlertType]="alert.type"
|
||||
[clrAlertAppLevel]="true"
|
||||
[(clrAlertClosed)]="alert.closed"
|
||||
(clrAlertClosedChange)="onAlertClose()"
|
||||
>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{ alert.message }}
|
||||
</span>
|
||||
</div>
|
||||
</clr-alert>
|
||||
</clr-alerts>
|
||||
@if (hasOpenAlert) {
|
||||
<clr-alerts>
|
||||
@for (alert of alerts; track alert) {
|
||||
<clr-alert
|
||||
[clrAlertType]="alert.type"
|
||||
[clrAlertAppLevel]="true"
|
||||
[(clrAlertClosed)]="alert.closed"
|
||||
(clrAlertClosedChange)="onAlertClose()"
|
||||
>
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{ alert.message }}
|
||||
</span>
|
||||
</div>
|
||||
</clr-alert>
|
||||
}
|
||||
</clr-alerts>
|
||||
}
|
||||
|
||||
@@ -41,12 +41,10 @@
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
<option
|
||||
*ngIf="options.children.length > 0 && enableLoadMore"
|
||||
data-type="load-more"
|
||||
class="load-more"
|
||||
>
|
||||
{{ loadingMore ? 'Loading...' : 'LOAD MORE' }}
|
||||
</option>
|
||||
@if (options.children.length > 0 && enableLoadMore) {
|
||||
<option data-type="load-more" class="load-more">
|
||||
{{ loadingMore ? 'Loading...' : 'LOAD MORE' }}
|
||||
</option>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,35 +6,37 @@
|
||||
>
|
||||
<h3 class="modal-title center text-center color-darker-gray">Dataset Meta</h3>
|
||||
<div class="modal-body">
|
||||
<p *ngIf="dsmetaTabs.length < 1" class="text-center">
|
||||
No dataset meta to show.
|
||||
</p>
|
||||
@if (dsmetaTabs.length < 1) {
|
||||
<p class="text-center">No dataset meta to show.</p>
|
||||
}
|
||||
|
||||
<clr-tabs clrLayout="vertical">
|
||||
<clr-tab *ngFor="let tab of tabs; let index = index">
|
||||
<button clrTabLink id="link1">{{ tab.name }}</button>
|
||||
<clr-tab-content
|
||||
id="content1"
|
||||
*clrIfActive="index === 0"
|
||||
class="d-flex clr-justify-content-center w-100"
|
||||
>
|
||||
<clr-datagrid>
|
||||
<ng-container *ngFor="let col of tab.colsToDisplay">
|
||||
<clr-dg-column>{{ col.colName || col.colKey }}</clr-dg-column>
|
||||
</ng-container>
|
||||
|
||||
<clr-dg-row
|
||||
(click)="tab.onRowClick ? tab.onRowClick(info) : ''"
|
||||
class="clickable-row"
|
||||
*ngFor="let info of tab.meta"
|
||||
>
|
||||
<ng-container *ngFor="let col of tab.colsToDisplay">
|
||||
<clr-dg-cell>{{ info[col.colKey] }}</clr-dg-cell>
|
||||
</ng-container>
|
||||
</clr-dg-row>
|
||||
</clr-datagrid>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
@for (tab of tabs; track tab; let index = $index) {
|
||||
<clr-tab>
|
||||
<button clrTabLink id="link1">{{ tab.name }}</button>
|
||||
<clr-tab-content
|
||||
id="content1"
|
||||
*clrIfActive="index === 0"
|
||||
class="d-flex clr-justify-content-center w-100"
|
||||
>
|
||||
<clr-datagrid>
|
||||
@for (col of tab.colsToDisplay; track col) {
|
||||
<clr-dg-column>{{ col.colName || col.colKey }}</clr-dg-column>
|
||||
}
|
||||
@for (info of tab.meta; track info) {
|
||||
<clr-dg-row
|
||||
(click)="tab.onRowClick ? tab.onRowClick(info) : ''"
|
||||
class="clickable-row"
|
||||
>
|
||||
@for (col of tab.colsToDisplay; track col) {
|
||||
<clr-dg-cell>{{ info[col.colKey] }}</clr-dg-cell>
|
||||
}
|
||||
</clr-dg-row>
|
||||
}
|
||||
</clr-datagrid>
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
}
|
||||
</clr-tabs>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -1,164 +1,180 @@
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="libraryList" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<ng-container *ngFor="let library of libraryList">
|
||||
<clr-tree-node
|
||||
#libTreeNode
|
||||
(click)="treeNodeClicked($event, library, libraryList)"
|
||||
*ngIf="!library['hidden'] && library['inForeground']"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<p
|
||||
(click)="
|
||||
lib = library.LIBRARYID;
|
||||
libraryOnClick(lib || '', library, libTreeNode)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
{{ library.LIBRARYNAME }}
|
||||
</p>
|
||||
|
||||
<clr-tree-node *ngIf="library['tables']" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (libraryList) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Libraries"
|
||||
name="input"
|
||||
[(ngModel)]="librariesSearch"
|
||||
(keyup)="libraryOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length > 0"
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables.TABLENAME')
|
||||
"
|
||||
(click)="librariesSearch = ''; libraryOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
|
||||
@for (library of libraryList; track library) {
|
||||
@if (!library['hidden'] && library['inForeground']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, libTable, library['tables'])"
|
||||
*ngFor="let libTable of library['tables']; let index = index"
|
||||
[(clrExpanded)]="libTable['expanded']"
|
||||
[clrLoading]="libTable['loadingColumns'] && !libTable.columns"
|
||||
[class.clr-expanded]="libTable['expanded']"
|
||||
#libTreeNode
|
||||
(click)="treeNodeClicked($event, library, libraryList)"
|
||||
[(clrExpanded)]="library['expanded']"
|
||||
[clrLoading]="library['loadingTables'] && !library.tables"
|
||||
[class.clr-expanded]="library['expanded']"
|
||||
>
|
||||
<clr-tooltip
|
||||
*ngVar="
|
||||
index + 1 >
|
||||
licenceState.value.tables_in_library_limit as tableLocked
|
||||
<p
|
||||
(click)="
|
||||
lib = library.LIBRARYID;
|
||||
libraryOnClick(lib || '', library, libTreeNode)
|
||||
"
|
||||
class="m-0 cursor-pointer"
|
||||
>
|
||||
<button
|
||||
clrTooltipTrigger
|
||||
(click)="
|
||||
!tableLocked
|
||||
? tableOnClick(libTable.TABLEURI, libTable, library)
|
||||
: ''
|
||||
"
|
||||
class="clr-treenode-link"
|
||||
[class.dc-locked-control]="tableLocked"
|
||||
[class.active]="libTabActive(library.LIBRARYREF, libTable)"
|
||||
>
|
||||
<ng-container [ngSwitch]="libTable.includes('-FC')">
|
||||
<clr-icon *ngSwitchCase="true" shape="bolt"></clr-icon>
|
||||
<clr-icon *ngSwitchCase="false" shape="table"></clr-icon>
|
||||
</ng-container>
|
||||
{{ libTable.replace('-FC', '') }}
|
||||
</button>
|
||||
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
<span *ngIf="tableLocked">
|
||||
To unlock all tables, contact support@datacontroller.io
|
||||
</span>
|
||||
</clr-tooltip-content>
|
||||
|
||||
<ng-container *ngIf="hasColumns">
|
||||
<clr-tree-node *ngIf="libTable['columns']" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Columns"
|
||||
name="input"
|
||||
[(ngModel)]="libTable['searchString']"
|
||||
(keyup)="treeOnFilter(libTable, 'columns.COLNAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon shape="rack-server"></clr-icon>
|
||||
{{ library.LIBRARYNAME }}
|
||||
</p>
|
||||
@if (library['tables']) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Tables"
|
||||
name="input"
|
||||
[(ngModel)]="library['searchString']"
|
||||
(keyup)="treeOnFilter(library, 'tables')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchTreeInput.value.length > 0"
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
libTable['searchString'] = '';
|
||||
treeOnFilter(libTable, 'columns.COLNAME')
|
||||
library['searchString'] = '';
|
||||
treeOnFilter(library, 'tables.TABLENAME')
|
||||
"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<clr-tree-node *ngFor="let libColumn of libTable['columns']">
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
@for (
|
||||
libTable of library['tables'];
|
||||
track libTable;
|
||||
let index = $index
|
||||
) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, libTable, library['tables'])"
|
||||
[(clrExpanded)]="libTable['expanded']"
|
||||
[clrLoading]="libTable['loadingColumns'] && !libTable.columns"
|
||||
[class.clr-expanded]="libTable['expanded']"
|
||||
>
|
||||
<clr-tooltip
|
||||
*ngVar="
|
||||
index + 1 >
|
||||
licenceState.value.tables_in_library_limit as tableLocked
|
||||
"
|
||||
>
|
||||
<button
|
||||
(click)="columnOnClick(libColumn, library, libTable)"
|
||||
clrTooltipTrigger
|
||||
(click)="
|
||||
!tableLocked
|
||||
? tableOnClick(libTable.TABLEURI, libTable, library)
|
||||
: ''
|
||||
"
|
||||
class="clr-treenode-link"
|
||||
[class.column-active]="libColumnActive(libColumn.COLURI)"
|
||||
[class.dc-locked-control]="tableLocked"
|
||||
[class.active]="libTabActive(library.LIBRARYREF, libTable)"
|
||||
>
|
||||
<clr-icon shape="objects"></clr-icon>
|
||||
|
||||
{{ libColumn.COLNAME }}
|
||||
@switch (libTable.includes('-FC')) {
|
||||
@case (true) {
|
||||
<clr-icon shape="bolt"></clr-icon>
|
||||
}
|
||||
@case (false) {
|
||||
<clr-icon shape="table"></clr-icon>
|
||||
}
|
||||
}
|
||||
{{ libTable.replace('-FC', '') }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
</clr-tooltip>
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-right"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
@if (tableLocked) {
|
||||
<span>
|
||||
To unlock all tables, contact support@datacontroller.io
|
||||
</span>
|
||||
}
|
||||
</clr-tooltip-content>
|
||||
@if (hasColumns) {
|
||||
@if (libTable['columns']) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchTreeInput
|
||||
placeholder="Columns"
|
||||
name="input"
|
||||
[(ngModel)]="libTable['searchString']"
|
||||
(keyup)="treeOnFilter(libTable, 'columns.COLNAME')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="
|
||||
searchTreeInput.value = '';
|
||||
libTable['searchString'] = '';
|
||||
treeOnFilter(libTable, 'columns.COLNAME')
|
||||
"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
}
|
||||
@for (libColumn of libTable['columns']; track libColumn) {
|
||||
<clr-tree-node>
|
||||
<button
|
||||
(click)="columnOnClick(libColumn, library, libTable)"
|
||||
class="clr-treenode-link"
|
||||
[class.column-active]="libColumnActive(libColumn.COLURI)"
|
||||
>
|
||||
<clr-icon shape="objects"></clr-icon>
|
||||
{{ libColumn.COLNAME }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tooltip>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
|
||||
<div *ngIf="librariesPaging" class="w-100 text-center">
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div>
|
||||
@if (librariesPaging) {
|
||||
<div class="w-100 text-center">
|
||||
<span class="spinner spinner-sm"> Loading... </span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ import { mapIntlCellTypes } from './utils/mapIntlCellTypes'
|
||||
import { CustomAutocompleteEditor } from './editors/numericAutocomplete'
|
||||
import { registerIntlCellTypes } from './cellTypes/intlCellTypes'
|
||||
import { makeNumberFormatRenderer } from '../../editor/utils/renderers.utils'
|
||||
import { makeRegexWarningRenderer } from '../../editor/utils/regex-warning-renderer'
|
||||
import { isRegexRuleExempt } from './utils/isRegexRuleExempt'
|
||||
import { parseRegexRule } from './utils/parseRegexRule'
|
||||
|
||||
export class DcValidator {
|
||||
private rules: DcValidation[] = []
|
||||
@@ -201,6 +204,29 @@ export class DcValidator {
|
||||
return isNaN(digits) ? undefined : digits
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RULE_VALUE of a HARDREGEX/SOFTREGEX rule on the given
|
||||
* column, for display in the column-header info dropdown. HARDREGEX wins
|
||||
* when a column has both, matching failsSoftRegex's own precedence (no
|
||||
* point surfacing the non-blocking pattern when the blocking one is what
|
||||
* actually governs the column).
|
||||
*
|
||||
* @param col column name
|
||||
*/
|
||||
getRegexRuleValue(col: string): string | undefined {
|
||||
const regexRule =
|
||||
this.dqrules.find(
|
||||
(rule: DQRule) =>
|
||||
rule.BASE_COL === col && rule.RULE_TYPE === 'HARDREGEX'
|
||||
) ??
|
||||
this.dqrules.find(
|
||||
(rule: DQRule) =>
|
||||
rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
|
||||
)
|
||||
|
||||
return regexRule?.RULE_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves dropdown source for given dc validation rule
|
||||
* The values comes from MPE_SELECTBOX table
|
||||
@@ -239,6 +265,32 @@ export class DcValidator {
|
||||
return details
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a value fails a SOFTREGEX rule on the given column, if one
|
||||
* exists. SOFTREGEX never goes through dqValidate/the cell validator (see
|
||||
* setupValidations — it's a display-only grid renderer instead), so the
|
||||
* edit-record modal, which has no grid renderer to hook into, uses this
|
||||
* directly to show the same warning outside the grid. HARDREGEX takes
|
||||
* precedence when both rules apply to the column, matching the grid's own
|
||||
* behaviour (setupValidations skips wiring the SOFTREGEX renderer there
|
||||
* too) — a dual-rule column should render red/blocked, never yellow.
|
||||
*/
|
||||
failsSoftRegex(col: string, value: any): boolean {
|
||||
if (this.hasDqRules(col, ['HARDREGEX'])) return false
|
||||
if (isRegexRuleExempt(value)) return false
|
||||
|
||||
const softRegexRule = this.dqrules.find(
|
||||
(rule: DQRule) => rule.BASE_COL === col && rule.RULE_TYPE === 'SOFTREGEX'
|
||||
)
|
||||
if (!softRegexRule) return false
|
||||
|
||||
try {
|
||||
return !parseRegexRule(softRegexRule.RULE_VALUE).test(value.toString())
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SOFTSELECT is not defined in DQ RULES
|
||||
* This function fetches it's values and pushes in the DQ RULES array with SOFTSELECT type
|
||||
@@ -395,6 +447,28 @@ export class DcValidator {
|
||||
// editor/validator stay intact via `type`.
|
||||
this.rules[i].numericFormat = undefined
|
||||
}
|
||||
|
||||
// SOFTREGEX: display-only warning (yellow), never blocks submission —
|
||||
// unlike HARDREGEX (see dq-validation.ts), which goes through the
|
||||
// normal validator/dqValidate path instead. Last-wins against
|
||||
// NUMBER_FORMAT if a column somehow carried both (not expected in
|
||||
// practice — one formats numbers, the other pattern-matches text).
|
||||
//
|
||||
// HARDREGEX takes precedence when both apply to the same column: skip
|
||||
// wiring this renderer entirely, so a failing value stays HOT's own
|
||||
// red htInvalid (from HARDREGEX/dqValidate) rather than being
|
||||
// visually overridden by this renderer's yellow dc-warning-cell.
|
||||
if (
|
||||
this.hasDqRules(ruleColName, ['SOFTREGEX']) &&
|
||||
!this.hasDqRules(ruleColName, ['HARDREGEX'])
|
||||
) {
|
||||
const softRegexRule = this.getDqDetails(ruleColName).find(
|
||||
(rule: DQRule) => rule.RULE_TYPE === 'SOFTREGEX'
|
||||
)
|
||||
this.rules[i].renderer = makeRegexWarningRenderer(
|
||||
softRegexRule?.RULE_VALUE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const self = this
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export interface Col {
|
||||
NAME: string
|
||||
VARNUM: number
|
||||
// No longer sent by getdata.sas (dropped to trim the COLS payload) —
|
||||
// column order in the array is authoritative. Optional for legacy responses.
|
||||
VARNUM?: number
|
||||
LABEL: string
|
||||
FMTNAME: string
|
||||
DDTYPE: string
|
||||
|
||||
@@ -18,3 +18,5 @@ export type DQRuleTypes =
|
||||
| 'HIDDEN'
|
||||
| 'ROUND'
|
||||
| 'NUMBER_FORMAT'
|
||||
| 'HARDREGEX'
|
||||
| 'SOFTREGEX'
|
||||
|
||||
@@ -524,6 +524,280 @@ describe('DC Validator', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('10 | blocks submission of a value that fails a HARDREGEX rule', () => {
|
||||
// SOME_CHAR already has its own CASE=UPCASE rule in example_dqRules —
|
||||
// 'AB1' passes that (it equals its own uppercase form) but fails a
|
||||
// letters-only HARDREGEX pattern, isolating HARDREGEX's own effect
|
||||
// rather than piggybacking on CASE rejecting the value too.
|
||||
const dcValidator: DcValidator = new DcValidator(
|
||||
example_sasparams,
|
||||
example_dataformats,
|
||||
example_cols,
|
||||
[
|
||||
...example_dqRules,
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
],
|
||||
example_dqData
|
||||
)
|
||||
const someCharRule = dcValidator.getRule('SOME_CHAR')
|
||||
|
||||
dcValidator.executeHotValidator(someCharRule!, 'ABC', (valid: boolean) => {
|
||||
expect(valid).toBeTrue()
|
||||
})
|
||||
dcValidator.executeHotValidator(someCharRule!, 'AB1', (valid: boolean) => {
|
||||
expect(valid).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
it('11 | wires a function renderer for a SOFTREGEX rule, without blocking submission', () => {
|
||||
// SOME_CHAR_ANY carries no other DQ rules in the shared fixture, so this
|
||||
// isolates SOFTREGEX's own wiring. The renderer's own pass/fail/delete-
|
||||
// suppression behaviour is covered by regex-warning-renderer.spec.ts —
|
||||
// this only proves setupValidations() assigns it and that, unlike
|
||||
// HARDREGEX, a non-matching value still submits.
|
||||
const dcValidator: DcValidator = new DcValidator(
|
||||
example_sasparams,
|
||||
example_dataformats,
|
||||
example_cols,
|
||||
[
|
||||
...example_dqRules,
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
],
|
||||
example_dqData
|
||||
)
|
||||
const someCharAnyRule = dcValidator.getRule('SOME_CHAR_ANY')
|
||||
|
||||
expect(typeof someCharAnyRule?.renderer).toEqual('function')
|
||||
|
||||
dcValidator.executeHotValidator(
|
||||
someCharAnyRule!,
|
||||
'not uppercase',
|
||||
(valid: boolean) => {
|
||||
expect(valid).toBeTrue()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('12 | HARDREGEX takes precedence over SOFTREGEX on a dual-rule column', () => {
|
||||
// Both rules on the same column: HARDREGEX must win, so the cell
|
||||
// renders red (blocked) via HOT's own htInvalid, not yellow. No
|
||||
// SOFTREGEX renderer is wired at all here, so there's nothing that could
|
||||
// visually compete with htInvalid for that column.
|
||||
const dcValidator: DcValidator = new DcValidator(
|
||||
example_sasparams,
|
||||
example_dataformats,
|
||||
example_cols,
|
||||
[
|
||||
...example_dqRules,
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
},
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
],
|
||||
example_dqData
|
||||
)
|
||||
const rule = dcValidator.getRule('SOME_CHAR_ANY')
|
||||
|
||||
dcValidator.executeHotValidator(
|
||||
rule!,
|
||||
'not uppercase',
|
||||
(valid: boolean) => {
|
||||
expect(valid).toBeFalse()
|
||||
}
|
||||
)
|
||||
expect(rule?.renderer).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('13 | failsSoftRegex (edit-record modal support for SOFTREGEX)', () => {
|
||||
// SOFTREGEX never goes through dqValidate (see the wiring in
|
||||
// setupValidations), so the edit-record modal — which has no grid
|
||||
// renderer to hook into — calls this directly to show the same warning.
|
||||
const buildValidator = (dqRules: DQRule[]) =>
|
||||
new DcValidator(
|
||||
example_sasparams,
|
||||
example_dataformats,
|
||||
example_cols,
|
||||
dqRules,
|
||||
example_dqData
|
||||
)
|
||||
|
||||
it('is true for a value that fails the pattern', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'lowercase')
|
||||
).toBeTrue()
|
||||
})
|
||||
|
||||
it('is false for a value that matches the pattern', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'UPPERCASE')
|
||||
).toBeFalse()
|
||||
})
|
||||
|
||||
it('handles a PRX-delimited pattern with a case-insensitive flag, as authored for prxparse', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '/\\b(the|data)\\b/i',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'this is dummy data')
|
||||
).toBeFalse()
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'THE WIND WAS BLOWING')
|
||||
).toBeFalse()
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'nothing relevant here')
|
||||
).toBeTrue()
|
||||
})
|
||||
|
||||
it('is false for a column with no SOFTREGEX rule', () => {
|
||||
const dcValidator = buildValidator([])
|
||||
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'anything')
|
||||
).toBeFalse()
|
||||
})
|
||||
|
||||
it('is false for blank/special-missing values, same exemption as HARDREGEX', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', '')).toBeFalse()
|
||||
expect(dcValidator.failsSoftRegex('SOME_CHAR_ANY', '.a')).toBeFalse()
|
||||
})
|
||||
|
||||
it('is false when the column also has HARDREGEX (precedence — no yellow on a red cell)', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
},
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(
|
||||
dcValidator.failsSoftRegex('SOME_CHAR_ANY', 'lowercase')
|
||||
).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('14 | getRegexRuleValue (column-header info display)', () => {
|
||||
const buildValidator = (dqRules: DQRule[]) =>
|
||||
new DcValidator(
|
||||
example_sasparams,
|
||||
example_dataformats,
|
||||
example_cols,
|
||||
dqRules,
|
||||
example_dqData
|
||||
)
|
||||
|
||||
it('returns the RULE_VALUE for a HARDREGEX rule', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '^[A-Z]+$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(dcValidator.getRegexRuleValue('SOME_CHAR_ANY')).toEqual('^[A-Z]+$')
|
||||
})
|
||||
|
||||
it('returns the RULE_VALUE for a SOFTREGEX rule', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '/\\b(the|data)\\b/i',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(dcValidator.getRegexRuleValue('SOME_CHAR_ANY')).toEqual(
|
||||
'/\\b(the|data)\\b/i'
|
||||
)
|
||||
})
|
||||
|
||||
it('prefers HARDREGEX over SOFTREGEX when a column has both', () => {
|
||||
const dcValidator = buildValidator([
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '^HARD$',
|
||||
X: 0
|
||||
},
|
||||
{
|
||||
BASE_COL: 'SOME_CHAR_ANY',
|
||||
RULE_TYPE: 'SOFTREGEX',
|
||||
RULE_VALUE: '^SOFT$',
|
||||
X: 0
|
||||
}
|
||||
])
|
||||
|
||||
expect(dcValidator.getRegexRuleValue('SOME_CHAR_ANY')).toEqual('^HARD$')
|
||||
})
|
||||
|
||||
it('returns undefined for a column with no regex rule', () => {
|
||||
const dcValidator = buildValidator([])
|
||||
|
||||
expect(dcValidator.getRegexRuleValue('SOME_CHAR_ANY')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/** Minimal cols[] entry — only the fields rule ordering depends on. */
|
||||
@@ -533,7 +807,7 @@ const makeCol = (name: string, varnum: number): Col =>
|
||||
VARNUM: varnum,
|
||||
LABEL: name,
|
||||
FMTNAME: '',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
TYPE: '',
|
||||
CLS_RULE: 'READ',
|
||||
MEMLABEL: '',
|
||||
@@ -655,7 +929,7 @@ const example_dqRules: any = [
|
||||
const example_cols = [
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
DESC: 'dropdown_desc',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -668,7 +942,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'NUMERIC',
|
||||
DDTYPE: 'N',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -681,7 +955,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'NUMERIC',
|
||||
DDTYPE: 'N',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -694,7 +968,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -707,7 +981,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -720,7 +994,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -733,7 +1007,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'CHARACTER',
|
||||
DDTYPE: 'C',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -785,7 +1059,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'NUMERIC',
|
||||
DDTYPE: 'N',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
@@ -798,7 +1072,7 @@ const example_cols = [
|
||||
},
|
||||
{
|
||||
CLS_RULE: 'READ',
|
||||
DDTYPE: 'NUMERIC',
|
||||
DDTYPE: 'N',
|
||||
DESC: '',
|
||||
TYPE: '',
|
||||
FMTNAME: '',
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isRegexRuleExempt } from './isRegexRuleExempt'
|
||||
|
||||
describe('isRegexRuleExempt', () => {
|
||||
it('exempts blank/undefined/null', () => {
|
||||
expect(isRegexRuleExempt('')).toBeTrue()
|
||||
expect(isRegexRuleExempt(undefined)).toBeTrue()
|
||||
expect(isRegexRuleExempt(null)).toBeTrue()
|
||||
})
|
||||
|
||||
it('exempts SAS special missing values', () => {
|
||||
expect(isRegexRuleExempt('.')).toBeTrue()
|
||||
expect(isRegexRuleExempt('.a')).toBeTrue()
|
||||
expect(isRegexRuleExempt('_')).toBeTrue()
|
||||
})
|
||||
|
||||
it('does not exempt an ordinary value', () => {
|
||||
expect(isRegexRuleExempt('ABC123')).toBeFalse()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { isSpecialMissing } from '@sasjs/utils/input/validators'
|
||||
|
||||
/**
|
||||
* HARDREGEX/SOFTREGEX both skip pattern-matching for blank and SAS special
|
||||
* missing values (., .a-.z, _) — the same exemption NOTNULL/MINVAL/MAXVAL
|
||||
* already apply elsewhere, since neither convention represents a real
|
||||
* formatted value the pattern is meant to check.
|
||||
*/
|
||||
export const isRegexRuleExempt = (value: any): boolean => {
|
||||
if (value === undefined || value === null || value === '') return true
|
||||
|
||||
return isSpecialMissing(value)
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { parseRegexRule } from './parseRegexRule'
|
||||
|
||||
describe('parseRegexRule', () => {
|
||||
it('treats a bare pattern (no delimiters) exactly as before', () => {
|
||||
const regex = parseRegexRule('^[A-Z]{3}\\d{4}$')
|
||||
|
||||
expect(regex.test('ABC1234')).toBeTrue()
|
||||
expect(regex.test('abc1234')).toBeFalse()
|
||||
})
|
||||
|
||||
it('parses a delimited pattern with no flags, case-sensitively', () => {
|
||||
const regex = parseRegexRule('/\\b(the|data)\\b/')
|
||||
|
||||
expect(regex.test('this is dummy data')).toBeTrue()
|
||||
expect(regex.test('THE WIND WAS BLOWING')).toBeFalse()
|
||||
})
|
||||
|
||||
it('parses a delimited pattern with the i flag, case-insensitively', () => {
|
||||
const regex = parseRegexRule('/\\b(the|data)\\b/i')
|
||||
|
||||
expect(regex.test('THIS IS DUMMY DATA')).toBeTrue()
|
||||
expect(regex.test('The wind was blowing')).toBeTrue()
|
||||
expect(regex.test('nothing relevant here')).toBeFalse()
|
||||
})
|
||||
|
||||
it('extracts source and flags correctly from a delimited pattern', () => {
|
||||
const regex = parseRegexRule('/^[A-Z]+$/g')
|
||||
|
||||
expect(regex.source).toEqual('^[A-Z]+$')
|
||||
expect(regex.flags).toEqual('g')
|
||||
})
|
||||
|
||||
it('treats a bare pattern containing an internal slash as a literal, not a delimited form', () => {
|
||||
const regex = parseRegexRule('a/b')
|
||||
|
||||
expect(regex.test('a/b')).toBeTrue()
|
||||
expect(regex.test('ab')).toBeFalse()
|
||||
})
|
||||
|
||||
it('throws for an unterminated delimited pattern, same as a bad bare pattern would', () => {
|
||||
expect(() => parseRegexRule('/[unterminated/')).toThrow()
|
||||
})
|
||||
|
||||
// parseRegexRule strips PRX's /pattern/flags delimiter wrapper and
|
||||
// translates the three PRX-only constructs below to a JS equivalent. These
|
||||
// three were picked because they're mechanical, low-risk translations with
|
||||
// one unambiguous JS equivalent. Atomic groups and possessive quantifiers are
|
||||
// NOT translated - both would need a lookahead+backreference rewrite that
|
||||
// changes the pattern's capture-group numbering, which is a real
|
||||
// correctness risk for comparatively rare constructs, so they're left as a
|
||||
// documented limitation below instead.
|
||||
describe('translated PRX-only syntax', () => {
|
||||
it('hoists a leading (?i) inline modifier into the i flag', () => {
|
||||
const regex = parseRegexRule('/(?i)abc/')
|
||||
|
||||
expect(regex.test('ABC')).toBeTrue()
|
||||
expect(regex.test('abc')).toBeTrue()
|
||||
})
|
||||
|
||||
it('merges a leading (?i) with flags already present after the closing delimiter', () => {
|
||||
const regex = parseRegexRule('/(?i)^[a-z]+$/g')
|
||||
|
||||
expect(regex.flags).toContain('i')
|
||||
expect(regex.flags).toContain('g')
|
||||
expect(regex.test('ABC')).toBeTrue()
|
||||
})
|
||||
|
||||
it('does not add a duplicate i flag when (?i) and the trailing /i are both present', () => {
|
||||
expect(() => parseRegexRule('/(?i)abc/i')).not.toThrow()
|
||||
})
|
||||
|
||||
it('treats \\Q...\\E as a literal, metacharacter-escaped sequence', () => {
|
||||
const regex = parseRegexRule('/\\Qa+b*c\\E/')
|
||||
|
||||
expect(regex.test('a+b*c')).toBeTrue()
|
||||
// Without the \Q...\E escaping, '+' and '*' would be quantifiers, so
|
||||
// this would also match a literal "c" alone. It shouldn't.
|
||||
expect(regex.test('c')).toBeFalse()
|
||||
})
|
||||
|
||||
it('translates \\A/\\z to absolute start/end anchors equivalent to PRX semantics', () => {
|
||||
const regex = parseRegexRule('/\\Afoo\\z/')
|
||||
|
||||
expect(regex.test('foo')).toBeTrue()
|
||||
expect(regex.test('foobar')).toBeFalse()
|
||||
expect(regex.test('barfoo')).toBeFalse()
|
||||
// \z (unlike JS's own $) does not match immediately before a
|
||||
// trailing newline - this is stricter, matching PRX/Perl semantics.
|
||||
expect(regex.test('foo\n')).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
// Pre-existing gaps, not regressions from this fix - both fail safely:
|
||||
// they throw, same as any other malformed pattern, and each call site
|
||||
// already treats a throw as "always valid" / "never warn".
|
||||
describe('known limitations - PRX-only syntax not translated', () => {
|
||||
it('throws on free-spacing/extended (/x) mode (valid PRX, not a JS flag)', () => {
|
||||
expect(() => parseRegexRule('/a+ \\s+ b/x')).toThrow()
|
||||
})
|
||||
|
||||
it('throws on an atomic group (?>...) (valid PRX, unsupported in JS)', () => {
|
||||
expect(() => parseRegexRule('/(?>a|ab)c/')).toThrow()
|
||||
})
|
||||
|
||||
it('throws on a possessive quantifier, e.g. a++ (valid PRX, unsupported in JS)', () => {
|
||||
expect(() => parseRegexRule('/a++b/')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
// HARDREGEX/SOFTREGEX RULE_VALUEs collected from an existing
|
||||
// MPE_VALIDATIONS table. None of these use PRX-only syntax (no inline
|
||||
// (?i), \Q...\E, \A/\z, atomic groups, or possessive quantifiers), so
|
||||
// they pass through parseRegexRule with only the /pattern/flags
|
||||
// delimiter stripped - the same PCRE-compatible escape rules apply on
|
||||
// both the SAS PRX side and the JS side, so behavior here is verified
|
||||
// to match, not just "doesn't throw".
|
||||
describe('RULE_VALUE corpus collected from an existing MPE_VALIDATIONS table', () => {
|
||||
const collectedRuleValues = [
|
||||
'/^-?[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
|
||||
'/^-?[0-9]{1,3}$/',
|
||||
'/^(?:(?!31\\\\.12).)*$/',
|
||||
'/^([0-9]{1,3}([.][0-9]{1,7})?)%$/',
|
||||
'/^(100|[0-9]{1,2})%$/',
|
||||
'/^[-,0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
|
||||
'/^[-0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
|
||||
'/^[-0-9]*([.]{0,1}[0-9]{0,2})$/',
|
||||
'/^[-0-9]*([.]{0,1}[0-9]{0,3})$/',
|
||||
'/^[-0-9]*([.]{0,1}[0-9]*)$/',
|
||||
'/^[-0-9]*$/',
|
||||
'/^[0-2]$/',
|
||||
'/^[0-3]$/',
|
||||
'/^[0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
|
||||
'/^[0-9]{1,36}$/',
|
||||
'/^[0-9]{1,72}$/',
|
||||
'/^[0-9]{4}$/',
|
||||
'/^[0-9]{5}$/',
|
||||
'/^[0-9]*([.]{0,1}[0-9]{0,4})$/',
|
||||
'/^[0,1]{1}([.]{0,1}[0-9]{0,4})$/',
|
||||
'/^[1]{1,1}$/',
|
||||
'/^[A-Z0-1]{5,5}$/',
|
||||
'/^[a-zA-Z]{1,1}$/',
|
||||
'/^[a-zA-Z]{1,3}$/',
|
||||
'/^[a-zA-Z0-9_]{0,36}$/',
|
||||
'/^[a-zA-Z0-9_=@\\/.:;,= -]{1,8}$/',
|
||||
'/^[a-zA-Z0-9@_-]{1,100}$/',
|
||||
'/^[a-zA-Z0-9@_-]{1,50}$/',
|
||||
'/^[a-zA-Z0-9=@.:;,= -_]{1,50}$/',
|
||||
'/^[a-zA-Z0-9=@.:;,= -_]{1,80}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,10}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,100}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,12}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,14}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,15}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,20}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,3}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,300}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,32}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,4}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,40}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,400}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,50}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,510}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,6}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,72}$/',
|
||||
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,8}$/',
|
||||
"/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,100}$/",
|
||||
"/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,20}$/",
|
||||
"/^[a-zA-Z0-9=@\\/.:;,= \\-_`']{1,500}$/",
|
||||
"/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/"
|
||||
]
|
||||
|
||||
collectedRuleValues.forEach((ruleValue) => {
|
||||
it(`compiles without throwing: ${ruleValue}`, () => {
|
||||
expect(() => parseRegexRule(ruleValue)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves the exact pattern body and applies no flags for a plain numeric-range rule', () => {
|
||||
const regex = parseRegexRule('/^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/')
|
||||
|
||||
expect(regex.source).toEqual('^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$')
|
||||
expect(regex.flags).toEqual('')
|
||||
expect(regex.test('1234.56')).toBeTrue()
|
||||
expect(regex.test('abc')).toBeFalse()
|
||||
})
|
||||
|
||||
it('handles the percentage-with-alternation rule (100 or 0-99, optionally with a decimal)', () => {
|
||||
const regex = parseRegexRule('/^(100|[0-9]{1,2})%$/')
|
||||
|
||||
expect(regex.test('100%')).toBeTrue()
|
||||
expect(regex.test('42%')).toBeTrue()
|
||||
expect(regex.test('101%')).toBeFalse()
|
||||
})
|
||||
|
||||
it('handles a character class with an escaped delimiter slash and a non-edge escaped hyphen', () => {
|
||||
const regex = parseRegexRule('/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/')
|
||||
|
||||
expect(regex.test('a/b-c d.e:f;g,h=i')).toBeTrue()
|
||||
expect(regex.test('has a # in it')).toBeFalse()
|
||||
})
|
||||
|
||||
it('handles a character class with literal backtick/single-quote characters', () => {
|
||||
const regex = parseRegexRule("/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/")
|
||||
|
||||
expect(regex.test("O'Brien`s value")).toBeTrue()
|
||||
// The unescaped space-to-underscore run (` -_`) in this class is a
|
||||
// range (space is code point 32, underscore 95), not three literal
|
||||
// characters - so it also matches '#' (35), which falls inside that
|
||||
// range. Same behavior in SAS PRX (identical character-class rules),
|
||||
// so this is a faithful translation, not a parseRegexRule bug.
|
||||
expect(regex.test('has a # in it')).toBeTrue()
|
||||
expect(regex.test('has a ~ in it')).toBeFalse()
|
||||
})
|
||||
|
||||
// This is the one inconsistency in the whole corpus: every other escaped
|
||||
// '/' in this file uses a single backslash, but this rule has \\. (two
|
||||
// backslashes) where every other sign points to a single \. having been
|
||||
// intended (a literal dot, inside a negative lookahead meant to exclude
|
||||
// "31.12" - e.g. a 31-December date - from matching). SAS PRX and JS
|
||||
// regex use identical backslash-escape semantics for this (no PRX-only
|
||||
// construct is involved), so parseRegexRule's output is a faithful,
|
||||
// parity-preserving translation of whatever was actually authored -
|
||||
// this test pins down what the corpus *actually* does today, verbatim,
|
||||
// not what was likely intended. \\ (escaped backslash) followed by an
|
||||
// unescaped . (any character) means the lookahead is functionally a
|
||||
// no-op: it only fails to match text containing a literal backslash,
|
||||
// which excludes nothing in practice, so every value - including dates
|
||||
// that literally contain "31.12" - passes.
|
||||
it('as authored (double backslash): the 31.12-exclusion lookahead is a no-op and matches everything', () => {
|
||||
const regex = parseRegexRule('/^(?:(?!31\\\\.12).)*$/')
|
||||
|
||||
expect(regex.test('31.12.2024')).toBeTrue()
|
||||
expect(regex.test('01.01.2024')).toBeTrue()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* HARDREGEX/SOFTREGEX rule values are authored as SAS PRX patterns, which
|
||||
* require the delimiter form `/pattern/flags` (prxparse's own syntax check
|
||||
* in mpe_validations_postedit.sas requires it). A bare pattern with no
|
||||
* delimiters is also accepted, for backwards compatibility with existing
|
||||
* rules/tests. Passing the delimited form as-is to `new RegExp(string)`
|
||||
* would treat the slashes and flags as literal pattern text instead of
|
||||
* stripping/applying them, so this parses the PRX form before constructing
|
||||
* the regex.
|
||||
*
|
||||
* PRX also accepts some Perl-only regex syntax with no direct JS equivalent.
|
||||
* Three mechanical, unambiguous cases are translated below - a leading
|
||||
* `(?i)` modifier, an escaped `\Q...\E` literal sequence, and `\A`/`\z`
|
||||
* absolute anchors.
|
||||
* Atomic groups (`(?>...)`) and possessive quantifiers (`a++`) are NOT
|
||||
* translated: both would need a lookahead+backreference rewrite that
|
||||
* changes the pattern's capture-group numbering, a real correctness risk
|
||||
* for comparatively rare constructs, so they're left to fail (they already
|
||||
* throw, which every caller already treats as "always valid" / "never
|
||||
* warn", same as any other malformed pattern).
|
||||
*/
|
||||
const escapeRegExpMetacharacters = (text: string): string =>
|
||||
text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
export const parseRegexRule = (ruleValue: string): RegExp => {
|
||||
const delimited = /^\/(.*)\/([a-z]*)$/s.exec(ruleValue)
|
||||
|
||||
if (!delimited) return new RegExp(ruleValue)
|
||||
|
||||
let [, body, flags] = delimited
|
||||
|
||||
const inlineCaseInsensitive = /^\(\?i\)/.exec(body)
|
||||
|
||||
if (inlineCaseInsensitive) {
|
||||
body = body.slice(inlineCaseInsensitive[0].length)
|
||||
if (!flags.includes('i')) flags += 'i'
|
||||
}
|
||||
|
||||
body = body.replace(/\\Q([\s\S]*?)\\E/g, (_match, literal) =>
|
||||
escapeRegExpMetacharacters(literal)
|
||||
)
|
||||
|
||||
body = body.replace(/\\A/g, '^').replace(/\\z/g, '(?![\\s\\S])')
|
||||
|
||||
return new RegExp(body, flags)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
import { dqValidate } from './dq-validation'
|
||||
|
||||
const rule = (overrides: Partial<DQRule>): DQRule => ({
|
||||
BASE_COL: 'SOME_CHAR',
|
||||
RULE_TYPE: 'HARDREGEX',
|
||||
RULE_VALUE: '',
|
||||
X: 0,
|
||||
...overrides
|
||||
})
|
||||
|
||||
describe('dqValidate - HARDREGEX', () => {
|
||||
it('accepts a value matching the pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '^[A-Z]{3}\\d{4}$' })]
|
||||
|
||||
expect(dqValidate(rules, 'ABC1234')).toBeTrue()
|
||||
})
|
||||
|
||||
it('rejects a value not matching the pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '^[A-Z]{3}\\d{4}$' })]
|
||||
|
||||
expect(dqValidate(rules, 'abc1234')).toBeFalse()
|
||||
})
|
||||
|
||||
it('treats blank/undefined/null as valid regardless of the pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '^[A-Z]{3}\\d{4}$' })]
|
||||
|
||||
expect(dqValidate(rules, '')).toBeTrue()
|
||||
expect(dqValidate(rules, undefined)).toBeTrue()
|
||||
expect(dqValidate(rules, null)).toBeTrue()
|
||||
})
|
||||
|
||||
it('treats SAS special missing values as valid regardless of the pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '^[A-Z]{3}\\d{4}$' })]
|
||||
|
||||
expect(dqValidate(rules, '.')).toBeTrue()
|
||||
expect(dqValidate(rules, '.a')).toBeTrue()
|
||||
expect(dqValidate(rules, '_')).toBeTrue()
|
||||
})
|
||||
|
||||
it('fails open (treats as valid) when the pattern is malformed', () => {
|
||||
const rules = [rule({ RULE_VALUE: '[unterminated' })]
|
||||
|
||||
expect(dqValidate(rules, 'anything')).toBeTrue()
|
||||
})
|
||||
|
||||
it('handles a more elaborate pattern (multiple character classes, quantifiers, an escaped literal dot)', () => {
|
||||
// Same email pattern used in the getdata.js mock's REGEX_HARD_COL demo.
|
||||
const rules = [rule({ RULE_VALUE: '[\\w.]+@[\\w]+\\.[a-z]{2,}' })]
|
||||
|
||||
expect(dqValidate(rules, 'user@example.com')).toBeTrue()
|
||||
expect(dqValidate(rules, 'user.name@sub.example.co')).toBeTrue()
|
||||
expect(dqValidate(rules, 'not-an-email')).toBeFalse()
|
||||
// No TLD to satisfy [a-z]{2,} after the escaped dot.
|
||||
expect(dqValidate(rules, 'user@example')).toBeFalse()
|
||||
})
|
||||
|
||||
it('accepts a value matching a PRX-delimited pattern (/pattern/flags, as authored for prxparse)', () => {
|
||||
const rules = [rule({ RULE_VALUE: '/^[A-Z]{3}\\d{4}$/' })]
|
||||
|
||||
expect(dqValidate(rules, 'ABC1234')).toBeTrue()
|
||||
})
|
||||
|
||||
it('rejects a value not matching a PRX-delimited pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '/^[A-Z]{3}\\d{4}$/' })]
|
||||
|
||||
expect(dqValidate(rules, 'abc1234')).toBeFalse()
|
||||
})
|
||||
|
||||
it('applies the i flag from a PRX-delimited pattern', () => {
|
||||
const rules = [rule({ RULE_VALUE: '/^[a-z]{3}\\d{4}$/i' })]
|
||||
|
||||
expect(dqValidate(rules, 'ABC1234')).toBeTrue()
|
||||
})
|
||||
|
||||
it('still evaluates a second, unrelated rule on the same column', () => {
|
||||
const rules = [
|
||||
rule({ RULE_VALUE: '^[A-Z]{3}\\d{4}$' }),
|
||||
rule({ RULE_TYPE: 'NOTNULL', RULE_VALUE: '' })
|
||||
]
|
||||
|
||||
// Passes HARDREGEX (blank is exempt) but fails NOTNULL — the NOTNULL
|
||||
// rule still runs even though HARDREGEX is earlier in the array.
|
||||
expect(dqValidate(rules, '')).toBeFalse()
|
||||
// Passes NOTNULL (non-blank) but fails HARDREGEX's pattern.
|
||||
expect(dqValidate(rules, 'nope')).toBeFalse()
|
||||
// Passes both.
|
||||
expect(dqValidate(rules, 'ABC1234')).toBeTrue()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DQRule } from '../models/dq-rules.model'
|
||||
import { specialMissingNumericValidator } from './hot-custom-validators'
|
||||
import { isRegexRuleExempt } from '../utils/isRegexRuleExempt'
|
||||
import { parseRegexRule } from '../utils/parseRegexRule'
|
||||
|
||||
const dqValidation: {
|
||||
[key: string]: (value: any, ruleValue: string | number) => boolean
|
||||
@@ -46,6 +48,20 @@ const dqValidation: {
|
||||
},
|
||||
NOTNULL: (value: any, ruleValue: string | number): boolean => {
|
||||
return value !== undefined && value !== null && value.toString().length > 0
|
||||
},
|
||||
// Pattern is used as authored, not auto-anchored — a rule author who
|
||||
// wants a full-value match must write ^...$ themselves.
|
||||
HARDREGEX: (value: any, ruleValue: string | number): boolean => {
|
||||
if (isRegexRuleExempt(value)) return true
|
||||
|
||||
try {
|
||||
return parseRegexRule(ruleValue.toString()).test(value.toString())
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`HARDREGEX - invalid pattern, treated as always-valid: ${ruleValue}`
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
<div *ngIf="options$ | async as options" class="excel-password-root">
|
||||
<clr-modal
|
||||
[clrModalOpen]="options.open"
|
||||
[clrModalSize]="'md'"
|
||||
[clrModalClosable]="false"
|
||||
>
|
||||
<h3 class="modal-title center text-center color-darker-gray">
|
||||
Password Protected File
|
||||
</h3>
|
||||
<div class="modal-body d-flex clr-justify-content-center">
|
||||
<p class="m-0">Please enter password:</p>
|
||||
<input
|
||||
#filePasswordInput
|
||||
[(ngModel)]="passwordInput"
|
||||
data-lpignore="true"
|
||||
autocomplete="off"
|
||||
id="filePasswordInput"
|
||||
type="text"
|
||||
class="clr-input disable-password-manager"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div>
|
||||
<p *ngIf="options.error" class="m-0 color-red">
|
||||
Sorry that didn't work, try again.
|
||||
</p>
|
||||
@if (options$ | async; as options) {
|
||||
<div class="excel-password-root">
|
||||
<clr-modal
|
||||
[clrModalOpen]="options.open"
|
||||
[clrModalSize]="'md'"
|
||||
[clrModalClosable]="false"
|
||||
>
|
||||
<h3 class="modal-title center text-center color-darker-gray">
|
||||
Password Protected File
|
||||
</h3>
|
||||
<div class="modal-body d-flex clr-justify-content-center">
|
||||
<p class="m-0">Please enter password:</p>
|
||||
<input
|
||||
#filePasswordInput
|
||||
[(ngModel)]="passwordInput"
|
||||
data-lpignore="true"
|
||||
autocomplete="off"
|
||||
id="filePasswordInput"
|
||||
type="text"
|
||||
class="clr-input disable-password-manager"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="button" class="btn btn-sm btn-outline" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success-outline"
|
||||
[disabled]="filePasswordInput.value.length < 1"
|
||||
(click)="close(filePasswordInput.value)"
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
<div class="modal-footer">
|
||||
<div>
|
||||
@if (options.error) {
|
||||
<p class="m-0 color-red">Sorry that didn't work, try again.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline"
|
||||
(click)="close()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success-outline"
|
||||
[disabled]="filePasswordInput.value.length < 1"
|
||||
(click)="close(filePasswordInput.value)"
|
||||
>
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</clr-modal>
|
||||
</div>
|
||||
</clr-modal>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -29,17 +29,21 @@
|
||||
></clr-icon>
|
||||
|
||||
<clr-signpost-content [clrPosition]="'bottom-middle'" *clrIfOpen>
|
||||
<p *ngFor="let request of requests">
|
||||
<code>{{ request.program }}</code>
|
||||
|
||||
<span class="running" *ngIf="request.running">loading</span>
|
||||
<span class="success" *ngIf="!request.running && request.successful"
|
||||
>loaded</span
|
||||
>
|
||||
<span class="fail" *ngIf="!request.running && !request.successful"
|
||||
>failed</span
|
||||
>
|
||||
</p>
|
||||
@for (request of requests; track request) {
|
||||
<p>
|
||||
<code>{{ request.program }}</code>
|
||||
|
||||
@if (request.running) {
|
||||
<span class="running">loading</span>
|
||||
}
|
||||
@if (!request.running && request.successful) {
|
||||
<span class="success">loaded</span>
|
||||
}
|
||||
@if (!request.running && !request.successful) {
|
||||
<span class="fail">failed</span>
|
||||
}
|
||||
</p>
|
||||
}
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
</div>
|
||||
|
||||
@@ -9,219 +9,252 @@
|
||||
<p class="mt-0 d-inline-block">App Location: {{ sasjsConfig.appLoc }}</p>
|
||||
</h3>
|
||||
<div class="modal-body h-70vh">
|
||||
<p *ngIf="sasjsRequests?.length! < 1" class="no-reqs">No requests made</p>
|
||||
@if (sasjsRequests.length < 1) {
|
||||
<p class="no-reqs">No requests made</p>
|
||||
}
|
||||
|
||||
<clr-stack-view *ngIf="opened && sasjsRequests?.length! > 0">
|
||||
<clr-stack-block
|
||||
*ngFor="let programLog of sasjsRequests; let i = index"
|
||||
[id]="'request_' + i"
|
||||
[clrStackViewLevel]="1"
|
||||
>
|
||||
<clr-stack-label>
|
||||
{{ programLog.serviceLink }}
|
||||
<span class="ml-10">
|
||||
<span *ngIf="programLog.logWarnings?.length! > 0">
|
||||
<clr-icon
|
||||
class="mb-5 color-green"
|
||||
shape="exclamation-circle"
|
||||
></clr-icon>
|
||||
</span>
|
||||
|
||||
<span *ngIf="programLog.logErrors?.length! > 0">
|
||||
<clr-icon
|
||||
class="mb-5 color-red"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
</span>
|
||||
</span>
|
||||
</clr-stack-label>
|
||||
<clr-stack-content>{{ programLog.parsedTimestamp }}</clr-stack-content>
|
||||
<clr-stack-block class="content">
|
||||
<clr-stack-content>
|
||||
<clr-tabs>
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Log</button>
|
||||
<ng-template [(clrIfActive)]="sasLogActive">
|
||||
<clr-tab-content>
|
||||
<clr-tree>
|
||||
<clr-tree-node
|
||||
*ngIf="programLog.logErrors"
|
||||
class="err-links"
|
||||
>
|
||||
<clr-icon
|
||||
class="color-red"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
Errors ({{ programLog.logErrors.length! }})
|
||||
<clr-tree-node
|
||||
(click)="goToLogLine(err, 'request_' + i, 'error')"
|
||||
*ngFor="let err of programLog.logErrors || []"
|
||||
>
|
||||
<button class="clr-treenode-link">
|
||||
{{ err }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
</clr-tree-node>
|
||||
|
||||
<clr-tree-node
|
||||
*ngIf="programLog.logWarnings"
|
||||
class="err-links"
|
||||
>
|
||||
<clr-icon
|
||||
class="color-green"
|
||||
shape="exclamation-circle"
|
||||
></clr-icon>
|
||||
Warnings ({{ programLog.logWarnings.length! }})
|
||||
<clr-tree-node
|
||||
(click)="
|
||||
goToLogLine(warning, 'request_' + i, 'warning')
|
||||
"
|
||||
*ngFor="let warning of programLog.logWarnings || []"
|
||||
>
|
||||
<button class="clr-treenode-link">
|
||||
{{ warning }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
</clr-tree-node>
|
||||
</clr-tree>
|
||||
|
||||
<div
|
||||
*ngIf="serverType !== 'SASVIYA'"
|
||||
class="log-wrapper saslog"
|
||||
[innerHTML]="programLog.logFile"
|
||||
></div>
|
||||
|
||||
<div
|
||||
*ngIf="serverType === 'SASVIYA'"
|
||||
class="log-wrapper saslog viya"
|
||||
[textContent]="programLog.logFile"
|
||||
></div>
|
||||
|
||||
<button
|
||||
*ngIf="programLog.logFile?.length! > 0"
|
||||
(click)="downloadLog(programLog.logFile)"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Source Code</button>
|
||||
<ng-template [(clrIfActive)]="sasSourceCodeActive">
|
||||
<clr-tab-content>
|
||||
<div class="log-wrapper">{{ programLog.sourceCode }}</div>
|
||||
<button
|
||||
*ngIf="programLog.sourceCode?.length! > 0"
|
||||
(click)="downloadSourceCode(programLog.sourceCode)"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Generated Code</button>
|
||||
<ng-template [(clrIfActive)]="sasGeneratedCodeActive">
|
||||
<clr-tab-content>
|
||||
<div class="log-wrapper">
|
||||
{{ programLog.generatedCode }}
|
||||
</div>
|
||||
<button
|
||||
*ngIf="programLog.generatedCode?.length! > 0"
|
||||
(click)="downloadGeneratedCode(programLog.generatedCode)"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
|
||||
<clr-tab *ngIf="programLog.SASWORK">
|
||||
<clr-dropdown
|
||||
[clrCloseMenuOnItemClick]="true"
|
||||
class="work-tables-dropdown"
|
||||
clrTabLink
|
||||
>
|
||||
<button clrDropdownTrigger>
|
||||
SAS Work Tables
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<div
|
||||
*ngFor="let table of programLog.SASWORK | keyvalue"
|
||||
(click)="programLog.selectedTable = $any(table).key"
|
||||
[class.selected]="programLog.selectedTable === table.key"
|
||||
class="dropdown-item"
|
||||
clrDropdownItem
|
||||
>
|
||||
{{ table.key }}
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
|
||||
<ng-template [(clrIfActive)]="tablesActive">
|
||||
<clr-tab-content>
|
||||
<p *ngIf="programLog.selectedTable" class="mt-10">
|
||||
<strong
|
||||
>{{ programLog.selectedTable }} (first 10 rows)
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<div class="min-h-50vh">
|
||||
<table
|
||||
*ngIf="programLog.selectedTable"
|
||||
class="table mt-10"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
*ngFor="
|
||||
let column of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows[0] | keyvalue
|
||||
"
|
||||
>
|
||||
{{ column.key }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="
|
||||
let row of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows
|
||||
"
|
||||
@if (opened && sasjsRequests.length > 0) {
|
||||
<clr-stack-view>
|
||||
@for (programLog of sasjsRequests; track programLog; let i = $index) {
|
||||
<clr-stack-block [id]="'request_' + i" [clrStackViewLevel]="1">
|
||||
<clr-stack-label>
|
||||
{{ programLog.serviceLink }}
|
||||
<span class="ml-10">
|
||||
@if (programLog.logWarnings?.length! > 0) {
|
||||
<span>
|
||||
<clr-icon
|
||||
class="mb-5 color-green"
|
||||
shape="exclamation-circle"
|
||||
></clr-icon>
|
||||
</span>
|
||||
}
|
||||
@if (programLog.logErrors?.length! > 0) {
|
||||
<span>
|
||||
<clr-icon
|
||||
class="mb-5 color-red"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</clr-stack-label>
|
||||
<clr-stack-content>{{
|
||||
programLog.parsedTimestamp
|
||||
}}</clr-stack-content>
|
||||
<clr-stack-block class="content">
|
||||
<clr-stack-content>
|
||||
<clr-tabs>
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Log</button>
|
||||
<ng-template [(clrIfActive)]="sasLogActive">
|
||||
<clr-tab-content>
|
||||
<clr-tree>
|
||||
@if (programLog.logErrors) {
|
||||
<clr-tree-node class="err-links">
|
||||
<clr-icon
|
||||
class="color-red"
|
||||
shape="exclamation-triangle"
|
||||
></clr-icon>
|
||||
Errors ({{ programLog.logErrors.length! }})
|
||||
@for (
|
||||
err of programLog.logErrors || [];
|
||||
track err
|
||||
) {
|
||||
<clr-tree-node
|
||||
(click)="
|
||||
goToLogLine(err, 'request_' + i, 'error')
|
||||
"
|
||||
>
|
||||
<button class="clr-treenode-link">
|
||||
{{ err }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
}
|
||||
@if (programLog.logWarnings) {
|
||||
<clr-tree-node class="err-links">
|
||||
<clr-icon
|
||||
class="color-green"
|
||||
shape="exclamation-circle"
|
||||
></clr-icon>
|
||||
Warnings ({{ programLog.logWarnings.length! }})
|
||||
@for (
|
||||
warning of programLog.logWarnings || [];
|
||||
track warning
|
||||
) {
|
||||
<clr-tree-node
|
||||
(click)="
|
||||
goToLogLine(
|
||||
warning,
|
||||
'request_' + i,
|
||||
'warning'
|
||||
)
|
||||
"
|
||||
>
|
||||
<button class="clr-treenode-link">
|
||||
{{ warning }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree-node>
|
||||
}
|
||||
</clr-tree>
|
||||
@if (serverType !== 'SASVIYA') {
|
||||
<div
|
||||
class="log-wrapper saslog"
|
||||
[innerHTML]="programLog.logFile"
|
||||
></div>
|
||||
}
|
||||
@if (serverType === 'SASVIYA') {
|
||||
<div
|
||||
class="log-wrapper saslog viya"
|
||||
[textContent]="programLog.logFile"
|
||||
></div>
|
||||
}
|
||||
@if (programLog.logFile.length > 0) {
|
||||
<button
|
||||
(click)="downloadLog(programLog.logFile)"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
<td
|
||||
*ngFor="
|
||||
let column of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows[0] | keyvalue
|
||||
Download
|
||||
</button>
|
||||
}
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Source Code</button>
|
||||
<ng-template [(clrIfActive)]="sasSourceCodeActive">
|
||||
<clr-tab-content>
|
||||
<div class="log-wrapper">
|
||||
{{ programLog.sourceCode }}
|
||||
</div>
|
||||
@if (programLog.sourceCode.length > 0) {
|
||||
<button
|
||||
(click)="downloadSourceCode(programLog.sourceCode)"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
}
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button clrTabLink>SAS Generated Code</button>
|
||||
<ng-template [(clrIfActive)]="sasGeneratedCodeActive">
|
||||
<clr-tab-content>
|
||||
<div class="log-wrapper">
|
||||
{{ programLog.generatedCode }}
|
||||
</div>
|
||||
@if (programLog.generatedCode.length > 0) {
|
||||
<button
|
||||
(click)="
|
||||
downloadGeneratedCode(programLog.generatedCode)
|
||||
"
|
||||
type="button"
|
||||
class="btn btn-link"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
}
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
@if (programLog.SASWORK) {
|
||||
<clr-tab>
|
||||
<clr-dropdown
|
||||
[clrCloseMenuOnItemClick]="true"
|
||||
class="work-tables-dropdown"
|
||||
clrTabLink
|
||||
>
|
||||
<button clrDropdownTrigger>
|
||||
SAS Work Tables
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
@for (
|
||||
table of programLog.SASWORK | keyvalue;
|
||||
track table
|
||||
) {
|
||||
<div
|
||||
(click)="
|
||||
programLog.selectedTable = $any(table).key
|
||||
"
|
||||
[class.selected]="
|
||||
programLog.selectedTable === table.key
|
||||
"
|
||||
class="dropdown-item"
|
||||
clrDropdownItem
|
||||
>
|
||||
{{ row[$any(column).key] }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
</clr-stack-content>
|
||||
</clr-stack-block>
|
||||
</clr-stack-block>
|
||||
</clr-stack-view>
|
||||
{{ table.key }}
|
||||
</div>
|
||||
}
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<ng-template [(clrIfActive)]="tablesActive">
|
||||
<clr-tab-content>
|
||||
@if (programLog.selectedTable) {
|
||||
<p class="mt-10">
|
||||
<strong
|
||||
>{{ programLog.selectedTable }} (first 10 rows)
|
||||
</strong>
|
||||
</p>
|
||||
}
|
||||
<div class="min-h-50vh">
|
||||
@if (programLog.selectedTable) {
|
||||
<table class="table mt-10">
|
||||
<thead>
|
||||
<tr>
|
||||
@for (
|
||||
column of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows[0] | keyvalue;
|
||||
track column
|
||||
) {
|
||||
<th>
|
||||
{{ column.key }}
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (
|
||||
row of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows;
|
||||
track row
|
||||
) {
|
||||
<tr>
|
||||
@for (
|
||||
column of programLog.SASWORK[
|
||||
programLog.selectedTable
|
||||
].first10rows[0] | keyvalue;
|
||||
track column
|
||||
) {
|
||||
<td>
|
||||
{{ row[$any(column).key] }}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</clr-tab-content>
|
||||
</ng-template>
|
||||
</clr-tab>
|
||||
}
|
||||
</clr-tabs>
|
||||
</clr-stack-content>
|
||||
</clr-stack-block>
|
||||
</clr-stack-block>
|
||||
}
|
||||
</clr-stack-view>
|
||||
}
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
@@ -6,143 +6,151 @@
|
||||
[style.max-width.px]="sidebarMaxWidth"
|
||||
[class.resizing]="resizing"
|
||||
>
|
||||
<div
|
||||
*ngIf="isMainRoute('view')"
|
||||
class="d-flex justify-content-center sub-dropdown"
|
||||
>
|
||||
<clr-dropdown>
|
||||
<button class="dropdown-toggle btn btn-primary" clrDropdownTrigger>
|
||||
{{ getSubPage() }}
|
||||
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<a
|
||||
*ngIf="isMainRoute('view')"
|
||||
clrVerticalNavLink
|
||||
(click)="navigateToViewer()"
|
||||
routerLinkActive="active"
|
||||
>Data</a
|
||||
@if (isMainRoute('view')) {
|
||||
<div class="d-flex justify-content-center sub-dropdown">
|
||||
<clr-dropdown>
|
||||
<button class="dropdown-toggle btn btn-primary" clrDropdownTrigger>
|
||||
{{ getSubPage() }}
|
||||
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
@if (isMainRoute('view')) {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
(click)="navigateToViewer()"
|
||||
routerLinkActive="active"
|
||||
>Data</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('view') && serverType === 'SASVIYA') {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/viya-api-explorer"
|
||||
routerLinkActive="active"
|
||||
>API Explorer</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('view') && serverType === 'SAS9') {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/lineage"
|
||||
routerLinkActive="active"
|
||||
>Lineage</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('view') && serverType === 'SAS9') {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/metadata"
|
||||
routerLinkActive="active"
|
||||
>Metadata</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('view')) {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/usernav"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
Users</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('approve')) {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/review/approve/submitted"
|
||||
routerLinkActive="active"
|
||||
>Submitted</a
|
||||
>
|
||||
}
|
||||
@if (isMainRoute('approve')) {
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/review/approve/toapprove"
|
||||
routerLinkActive="active"
|
||||
>To Approve</a
|
||||
>
|
||||
}
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
}
|
||||
@if (isMainRoute('view/usernav')) {
|
||||
<clr-tabs class="p-10">
|
||||
<clr-tab>
|
||||
<button
|
||||
[routerLink]="['/view/usernav/users']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('view') && serverType === 'SASVIYA'"
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/viya-api-explorer"
|
||||
routerLinkActive="active"
|
||||
>API Explorer</a
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="user"></clr-icon> Users
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content> </clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button
|
||||
[routerLink]="['/view/usernav/groups']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('view') && serverType === 'SAS9'"
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/lineage"
|
||||
routerLinkActive="active"
|
||||
>Lineage</a
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('view') && serverType === 'SAS9'"
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/metadata"
|
||||
routerLinkActive="active"
|
||||
>Metadata</a
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('view')"
|
||||
clrVerticalNavLink
|
||||
routerLink="/view/usernav"
|
||||
routerLinkActive="active"
|
||||
>
|
||||
Users</a
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('approve')"
|
||||
clrVerticalNavLink
|
||||
routerLink="/review/approve/submitted"
|
||||
routerLinkActive="active"
|
||||
>Submitted</a
|
||||
>
|
||||
<a
|
||||
*ngIf="isMainRoute('approve')"
|
||||
clrVerticalNavLink
|
||||
routerLink="/review/approve/toapprove"
|
||||
routerLinkActive="active"
|
||||
>To Approve</a
|
||||
>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<clr-tabs class="p-10" *ngIf="isMainRoute('view/usernav')">
|
||||
<clr-tab>
|
||||
<button
|
||||
[routerLink]="['/view/usernav/users']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="user"></clr-icon> Users
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content> </clr-tab-content>
|
||||
</clr-tab>
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="users"></clr-icon> Groups
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive="isMainRoute('/view/usernav/groups')">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
@if (serverType === 'SAS9') {
|
||||
<clr-tab>
|
||||
<button
|
||||
[routerLink]="['/view/usernav/roles']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="blocks-group"></clr-icon>
|
||||
Roles
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive="isMainRoute('/view/usernav/roles')">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
}
|
||||
</clr-tabs>
|
||||
}
|
||||
|
||||
<clr-tab>
|
||||
<button
|
||||
[routerLink]="['/view/usernav/groups']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="users"></clr-icon> Groups
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive="isMainRoute('/view/usernav/groups')">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
|
||||
<clr-tab *ngIf="serverType === 'SAS9'">
|
||||
<button
|
||||
[routerLink]="['/view/usernav/roles']"
|
||||
class="user-nav-btn"
|
||||
clrTabLink
|
||||
>
|
||||
<p class="zero-margin">
|
||||
<clr-icon aria-hidden="true" shape="blocks-group"></clr-icon> Roles
|
||||
</p>
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive="isMainRoute('/view/usernav/roles')">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
|
||||
<div
|
||||
*ngIf="isMainRoute('home')"
|
||||
class="d-flex justify-content-center sub-dropdown"
|
||||
>
|
||||
<clr-dropdown>
|
||||
<button class="dropdown-toggle btn btn-primary" clrDropdownTrigger>
|
||||
{{ getSubPage() }}
|
||||
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/tables"
|
||||
routerLinkActive="active"
|
||||
>Tables</a
|
||||
>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/excel-maps"
|
||||
routerLinkActive="active"
|
||||
>Excel Maps</a
|
||||
>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/multi-load"
|
||||
routerLinkActive="active"
|
||||
>Multi Load</a
|
||||
>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
@if (isMainRoute('home')) {
|
||||
<div class="d-flex justify-content-center sub-dropdown">
|
||||
<clr-dropdown>
|
||||
<button class="dropdown-toggle btn btn-primary" clrDropdownTrigger>
|
||||
{{ getSubPage() }}
|
||||
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu *clrIfOpen>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/tables"
|
||||
routerLinkActive="active"
|
||||
>Tables</a
|
||||
>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/excel-maps"
|
||||
routerLinkActive="active"
|
||||
>Excel Maps</a
|
||||
>
|
||||
<a
|
||||
clrVerticalNavLink
|
||||
routerLink="/home/multi-load"
|
||||
routerLinkActive="active"
|
||||
>Multi Load</a
|
||||
>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
<label
|
||||
*ngIf="label"
|
||||
[class.secondLabelActive]="secondLabel && secondLabel.length > 0"
|
||||
class="clr-control-label"
|
||||
>
|
||||
<span
|
||||
(click)="onChangeLabel('first')"
|
||||
[class.value-type-selected]="labelSelected === 'first'"
|
||||
>{{ label }}</span
|
||||
@if (label) {
|
||||
<label
|
||||
[class.secondLabelActive]="secondLabel && secondLabel.length > 0"
|
||||
class="clr-control-label"
|
||||
>
|
||||
<ng-container *ngIf="secondLabel">
|
||||
/
|
||||
<span
|
||||
(click)="onChangeLabel('second')"
|
||||
[class.value-type-selected]="labelSelected === 'second'"
|
||||
>{{ secondLabel }}</span
|
||||
(click)="onChangeLabel('first')"
|
||||
[class.value-type-selected]="labelSelected === 'first'"
|
||||
>{{ label }}</span
|
||||
>
|
||||
</ng-container>
|
||||
</label>
|
||||
<ng-container [ngSwitch]="type">
|
||||
<ng-container *ngSwitchCase="'date'">
|
||||
@if (secondLabel) {
|
||||
/
|
||||
<span
|
||||
(click)="onChangeLabel('second')"
|
||||
[class.value-type-selected]="labelSelected === 'second'"
|
||||
>{{ secondLabel }}</span
|
||||
>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
@switch (type) {
|
||||
@case ('date') {
|
||||
<clr-date-container>
|
||||
<input
|
||||
type="date"
|
||||
@@ -36,8 +37,8 @@
|
||||
[disabled]="this.disabled"
|
||||
/>
|
||||
</clr-date-container>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'time'">
|
||||
}
|
||||
@case ('time') {
|
||||
<input
|
||||
type="time"
|
||||
step="1"
|
||||
@@ -53,8 +54,8 @@
|
||||
(mouseout)="onInputMouseOut()"
|
||||
[disabled]="this.disabled"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'text'">
|
||||
}
|
||||
@case ('text') {
|
||||
<app-autocomplete
|
||||
#input
|
||||
class="input-val"
|
||||
@@ -71,5 +72,5 @@
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</app-autocomplete>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,8 @@
|
||||
<label>I have read and agree to the terms of above.</label>
|
||||
</clr-checkbox-wrapper>
|
||||
|
||||
<clr-spinner
|
||||
*ngIf="agreeBoxLocked && agreeClicked"
|
||||
clrInline="small"
|
||||
></clr-spinner>
|
||||
@if (agreeBoxLocked && agreeClicked) {
|
||||
<clr-spinner clrInline="small"></clr-spinner>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
<div class="header-actions">
|
||||
<div class="d-flex clr-align-items-center">
|
||||
<cds-icon
|
||||
*ngIf="!darkMode.value"
|
||||
(click)="toggleDarkMode(true)"
|
||||
size="md"
|
||||
class="theme-toggle-icon cursor-pointer"
|
||||
shape="moon"
|
||||
inverse="false"
|
||||
solid="false"
|
||||
></cds-icon>
|
||||
<cds-icon
|
||||
*ngIf="!!darkMode.value"
|
||||
(click)="toggleDarkMode(false)"
|
||||
size="md"
|
||||
class="theme-toggle-icon cursor-pointer"
|
||||
shape="sun"
|
||||
inverse="false"
|
||||
solid="false"
|
||||
></cds-icon>
|
||||
@if (!darkMode.value) {
|
||||
<cds-icon
|
||||
(click)="toggleDarkMode(true)"
|
||||
size="md"
|
||||
class="theme-toggle-icon cursor-pointer"
|
||||
shape="moon"
|
||||
inverse="false"
|
||||
solid="false"
|
||||
></cds-icon>
|
||||
}
|
||||
@if (!!darkMode.value) {
|
||||
<cds-icon
|
||||
(click)="toggleDarkMode(false)"
|
||||
size="md"
|
||||
class="theme-toggle-icon cursor-pointer"
|
||||
shape="sun"
|
||||
inverse="false"
|
||||
solid="false"
|
||||
></cds-icon>
|
||||
}
|
||||
</div>
|
||||
|
||||
<app-loading-indicator></app-loading-indicator>
|
||||
@@ -25,29 +27,31 @@
|
||||
<clr-dropdown class="app-nav-dropdown">
|
||||
<button class="nav-text color-white user-nav-toggle" clrDropdownToggle>
|
||||
<span>{{ userName }}</span>
|
||||
<span *ngIf="userName !== 'Not logged in' && isViya"
|
||||
><img
|
||||
class="avatar-img"
|
||||
src="{{ getPictureUrl() }}"
|
||||
alt="profile picture"
|
||||
/></span>
|
||||
<span
|
||||
class="badge badge-danger"
|
||||
*ngIf="!sasjsConfig.debug"
|
||||
[class.hidden]="failedReqs.length === 0"
|
||||
>{{ failedReqs.length }}</span
|
||||
>
|
||||
<span
|
||||
class="badge badge-info"
|
||||
*ngIf="sasjsConfig.debug"
|
||||
[class.hidden]="debugLogs.length === 0"
|
||||
>{{ debugLogs.length }}</span
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="!isViya"
|
||||
aria-hidden="true"
|
||||
shape="caret down"
|
||||
></clr-icon>
|
||||
@if (userName !== 'Not logged in' && isViya) {
|
||||
<span
|
||||
><img
|
||||
class="avatar-img"
|
||||
src="{{ getPictureUrl() }}"
|
||||
alt="profile picture"
|
||||
/></span>
|
||||
}
|
||||
@if (!sasjsConfig.debug) {
|
||||
<span
|
||||
class="badge badge-danger"
|
||||
[class.hidden]="failedReqs.length === 0"
|
||||
>{{ failedReqs.length }}</span
|
||||
>
|
||||
}
|
||||
@if (sasjsConfig.debug) {
|
||||
<span
|
||||
class="badge badge-info"
|
||||
[class.hidden]="debugLogs.length === 0"
|
||||
>{{ debugLogs.length }}</span
|
||||
>
|
||||
}
|
||||
@if (!isViya) {
|
||||
<clr-icon aria-hidden="true" shape="caret down"></clr-icon>
|
||||
}
|
||||
</button>
|
||||
<clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
|
||||
<div #dropdownItemDebug class="debug-switch-item" clrDropdownItem>
|
||||
@@ -71,7 +75,7 @@
|
||||
<span>SAS Requests</span>
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="!isDeployPage">
|
||||
@if (!isDeployPage) {
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://docs.datacontroller.io"
|
||||
@@ -79,7 +83,7 @@
|
||||
>
|
||||
<span class="dropdown-text">Documentation</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<div class="separator"></div>
|
||||
<a href="..." routerLink="/system" clrDropdownItem>
|
||||
|
||||
@@ -18,4 +18,28 @@ describe('buildColInfoHtml', () => {
|
||||
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.'
|
||||
)
|
||||
})
|
||||
|
||||
it('appends a REGEX line when a regex rule value is provided', () => {
|
||||
const colInfo: DataFormat = {
|
||||
label: 'Some Character Column',
|
||||
type: 'char',
|
||||
length: '1024',
|
||||
format: '$1024.'
|
||||
}
|
||||
|
||||
expect(buildColInfoHtml('SOME_CHAR', colInfo, '/^[A-Z]+$/i')).toBe(
|
||||
'NAME: SOME_CHAR<br>LABEL: Some Character Column<br>TYPE: char<br>LENGTH: 1024<br>FORMAT: $1024.<br>REGEX: /^[A-Z]+$/i'
|
||||
)
|
||||
})
|
||||
|
||||
it('omits the REGEX line when no regex rule value is provided', () => {
|
||||
const colInfo: DataFormat = {
|
||||
label: 'Some Character Column',
|
||||
type: 'char',
|
||||
length: '1024',
|
||||
format: '$1024.'
|
||||
}
|
||||
|
||||
expect(buildColInfoHtml('SOME_CHAR', colInfo)).not.toContain('REGEX:')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,9 +7,14 @@ import { DataFormat } from '../../models/sas/common/DateFormat'
|
||||
*/
|
||||
export function buildColInfoHtml(
|
||||
colName: string,
|
||||
colInfo?: DataFormat
|
||||
colInfo?: DataFormat,
|
||||
regexRuleValue?: string
|
||||
): string {
|
||||
if (!colInfo) return 'No info found'
|
||||
|
||||
return `NAME: ${colName}<br>LABEL: ${colInfo.label}<br>TYPE: ${colInfo.type}<br>LENGTH: ${colInfo.length}<br>FORMAT: ${colInfo.format}`
|
||||
let html = `NAME: ${colName}<br>LABEL: ${colInfo.label}<br>TYPE: ${colInfo.type}<br>LENGTH: ${colInfo.length}<br>FORMAT: ${colInfo.format}`
|
||||
|
||||
if (regexRuleValue) html += `<br>REGEX: ${regexRuleValue}`
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
>
|
||||
<h3 class="modal-title">
|
||||
Viewboxes
|
||||
<span
|
||||
*ngIf="licenceState.value.viewbox_limit !== Infinity"
|
||||
class="licence-notice"
|
||||
>To unlock more then {{ licenceState.value.viewbox_limit }}
|
||||
{{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
|
||||
contact support@datacontroller.io</span
|
||||
>
|
||||
@if (licenceState.value.viewbox_limit !== Infinity) {
|
||||
<span class="licence-notice"
|
||||
>To unlock more then {{ licenceState.value.viewbox_limit }}
|
||||
{{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
|
||||
contact support@datacontroller.io</span
|
||||
>
|
||||
}
|
||||
</h3>
|
||||
|
||||
<div class="modal-body d-flex">
|
||||
@@ -23,20 +23,23 @@
|
||||
|
||||
<hr class="w-100" />
|
||||
|
||||
<div *ngIf="librariesLoading" class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
@if (librariesLoading) {
|
||||
<div class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p *ngIf="viewboxLimitReached" class="m-0 viewbox-limit-notice">
|
||||
Viewbox open limit reached
|
||||
</p>
|
||||
@if (viewboxLimitReached) {
|
||||
<p class="m-0 viewbox-limit-notice">Viewbox open limit reached</p>
|
||||
}
|
||||
|
||||
<dc-tree
|
||||
*ngIf="viewboxModal"
|
||||
[libraryList]="libraries"
|
||||
(libraryOnClickEmitter)="libraryOnClick($event)"
|
||||
(tableOnClickEmitter)="tableOnClick($event)"
|
||||
></dc-tree>
|
||||
@if (viewboxModal) {
|
||||
<dc-tree
|
||||
[libraryList]="libraries"
|
||||
(libraryOnClickEmitter)="libraryOnClick($event)"
|
||||
(tableOnClickEmitter)="tableOnClick($event)"
|
||||
></dc-tree>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="currently-open">
|
||||
@@ -82,78 +85,77 @@
|
||||
|
||||
<hr />
|
||||
|
||||
<p *ngIf="viewboxes.length < 1" class="text-center w-100 m-0">
|
||||
Pick table from the list on the left
|
||||
</p>
|
||||
@if (viewboxes.length < 1) {
|
||||
<p class="text-center w-100 m-0">
|
||||
Pick table from the list on the left
|
||||
</p>
|
||||
}
|
||||
|
||||
<div
|
||||
*ngFor="let viewbox of viewboxes"
|
||||
(click)="selectViewbox(viewbox)"
|
||||
[class.selected]="selectedViewbox?.id === viewbox.id"
|
||||
class="open-viewbox d-flex clr-justify-content-between clr-align-items-center"
|
||||
>
|
||||
<div class="d-flex clr-align-items-center">
|
||||
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
|
||||
<clr-spinner
|
||||
*ngIf="viewbox.loadingData"
|
||||
clrSmall
|
||||
class="ml-3"
|
||||
></clr-spinner>
|
||||
@for (viewbox of viewboxes; track viewbox) {
|
||||
<div
|
||||
(click)="selectViewbox(viewbox)"
|
||||
[class.selected]="selectedViewbox?.id === viewbox.id"
|
||||
class="open-viewbox d-flex clr-justify-content-between clr-align-items-center"
|
||||
>
|
||||
<div class="d-flex clr-align-items-center">
|
||||
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
|
||||
@if (viewbox.loadingData) {
|
||||
<clr-spinner clrSmall class="ml-3"></clr-spinner>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
(click)="resetPosSize(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left"
|
||||
[class.disabled]="viewbox.x === 0 && viewbox.y === 0"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="cursor-move"
|
||||
size="13"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Reset position and size</span>
|
||||
</a>
|
||||
@if (!viewbox.minimized) {
|
||||
<a
|
||||
(click)="minimize(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon aria-hidden="true" shape="minus" size="19"></clr-icon>
|
||||
<span class="tooltip-content">Minimize viewbox</span>
|
||||
</a>
|
||||
}
|
||||
@if (viewbox.minimized) {
|
||||
<a
|
||||
(click)="restore(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="window-restore"
|
||||
size="19"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Restore viewbox</span>
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
(click)="close(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon aria-hidden="true" shape="times" size="19"></clr-icon>
|
||||
<span class="tooltip-content">Close viewbox</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
(click)="resetPosSize(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left"
|
||||
[class.disabled]="viewbox.x === 0 && viewbox.y === 0"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="cursor-move"
|
||||
size="13"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Reset position and size</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="!viewbox.minimized"
|
||||
(click)="minimize(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon aria-hidden="true" shape="minus" size="19"></clr-icon>
|
||||
<span class="tooltip-content">Minimize viewbox</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
*ngIf="viewbox.minimized"
|
||||
(click)="restore(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="window-restore"
|
||||
size="19"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content">Restore viewbox</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
(click)="close(viewbox); stopPropagation($event)"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-md tooltip-bottom-left ml-3"
|
||||
>
|
||||
<clr-icon aria-hidden="true" shape="times" size="19"></clr-icon>
|
||||
<span class="tooltip-content">Close viewbox</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
class="d-flex clr-justify-content-between clr-align-items-center mt-10"
|
||||
@@ -186,7 +188,7 @@
|
||||
} as hotColumns"
|
||||
cdkDropListGroup
|
||||
>
|
||||
<ng-container *ngIf="hotColumns.colHeadersHidden">
|
||||
@if (hotColumns.colHeadersHidden) {
|
||||
<div class="cols-search">
|
||||
<!-- <input #columnSearch clrInput (input)="columnSearchChange($event, selectedViewboxTable)" placeholder="Search columns"/> -->
|
||||
<app-autocomplete
|
||||
@@ -202,14 +204,16 @@
|
||||
onColsearchChange(input, $event, hotColumns.colHeadersHidden)
|
||||
"
|
||||
>
|
||||
<option *ngFor="let column of hotColumns.colHeadersHidden">
|
||||
{{ column }}
|
||||
</option>
|
||||
@for (column of hotColumns.colHeadersHidden; track column) {
|
||||
<option>
|
||||
{{ column }}
|
||||
</option>
|
||||
}
|
||||
</app-autocomplete>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="hotColumns.colHeadersVisible">
|
||||
@if (hotColumns.colHeadersVisible) {
|
||||
<p class="mt-0">Columns To Display (drag and re-order)</p>
|
||||
<div
|
||||
class="cols-list"
|
||||
@@ -217,42 +221,41 @@
|
||||
[cdkDropListData]="hotColumns.colHeadersVisible"
|
||||
(cdkDropListDropped)="columnsDrop($event)"
|
||||
>
|
||||
<div
|
||||
class="col-box column-{{ column }}"
|
||||
*ngFor="let column of hotColumns.colHeadersVisible"
|
||||
[class.cursor-default-i]="hotColumns.headerPks.includes(column)"
|
||||
[class.primaryKeyHeaderStyle]="
|
||||
hotColumns.headerPks.includes(column)
|
||||
"
|
||||
[cdkDragData]="column"
|
||||
[cdkDragDisabled]="hotColumns.headerPks.includes(column)"
|
||||
cdkDrag
|
||||
>
|
||||
<span>
|
||||
{{ column }}
|
||||
{{
|
||||
selectedViewboxTable?.hotTable?.$dataformats.vars[column]
|
||||
.type === 'num'
|
||||
? '(numeric)'
|
||||
: ''
|
||||
}}
|
||||
<clr-icon
|
||||
*ngIf="hotColumns.headerPks.includes(column)"
|
||||
aria-hidden="true"
|
||||
shape="key"
|
||||
></clr-icon>
|
||||
</span>
|
||||
|
||||
<clr-icon
|
||||
*ngIf="!hotColumns.headerPks.includes(column)"
|
||||
(click)="onColRemove(column)"
|
||||
aria-label="Remove column"
|
||||
shape="trash"
|
||||
class="cursor-pointer"
|
||||
></clr-icon>
|
||||
</div>
|
||||
@for (column of hotColumns.colHeadersVisible; track column) {
|
||||
<div
|
||||
class="col-box column-{{ column }}"
|
||||
[class.cursor-default-i]="hotColumns.headerPks.includes(column)"
|
||||
[class.primaryKeyHeaderStyle]="
|
||||
hotColumns.headerPks.includes(column)
|
||||
"
|
||||
[cdkDragData]="column"
|
||||
[cdkDragDisabled]="hotColumns.headerPks.includes(column)"
|
||||
cdkDrag
|
||||
>
|
||||
<span>
|
||||
{{ column }}
|
||||
{{
|
||||
selectedViewboxTable?.hotTable?.$dataformats?.vars?.[column]
|
||||
?.type === 'num'
|
||||
? '(numeric)'
|
||||
: ''
|
||||
}}
|
||||
@if (hotColumns.headerPks.includes(column)) {
|
||||
<clr-icon aria-hidden="true" shape="key"></clr-icon>
|
||||
}
|
||||
</span>
|
||||
@if (!hotColumns.headerPks.includes(column)) {
|
||||
<clr-icon
|
||||
(click)="onColRemove(column)"
|
||||
aria-label="Remove column"
|
||||
shape="trash"
|
||||
class="cursor-pointer"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,158 +271,159 @@
|
||||
</clr-modal>
|
||||
|
||||
<div class="viewboxes-container">
|
||||
<div
|
||||
*ngFor="let viewbox of viewboxes; let index = index"
|
||||
(mousedown)="focusViewbox(viewbox)"
|
||||
[class.focused]="viewbox.focused"
|
||||
[style.display]="viewbox.minimized ? 'none' : ''"
|
||||
[style.width.px]="viewbox.width"
|
||||
[style.height.px]="viewbox.height"
|
||||
[style.pointer-events]="viewbox.collapsed ? 'none' : ''"
|
||||
[style.box-shadow]="viewbox.collapsed ? 'none' : ''"
|
||||
class="viewbox"
|
||||
cdkDragBoundary=".viewboxes-container"
|
||||
cdkDrag
|
||||
[cdkDragFreeDragPosition]="{ x: viewbox.x, y: viewbox.y }"
|
||||
(cdkDragEnded)="viewboxDragEnded($event, viewbox)"
|
||||
tabindex="0"
|
||||
[id]="'viewbox_' + viewbox.id"
|
||||
#resizeBox
|
||||
>
|
||||
@for (viewbox of viewboxes; track viewbox; let index = $index) {
|
||||
<div
|
||||
class="drag-handle d-flex clr-justify-content-between clr-align-items-center"
|
||||
cdkDragHandle
|
||||
>
|
||||
<span class="table-title">
|
||||
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
|
||||
</span>
|
||||
|
||||
<div class="actions">
|
||||
<clr-icon
|
||||
*ngIf="!viewbox.collapsed"
|
||||
(click)="collapse(viewbox)"
|
||||
aria-label="Collapse"
|
||||
shape="angle top"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="viewbox.collapsed"
|
||||
(click)="expand(viewbox)"
|
||||
aria-label="Expand"
|
||||
shape="angle down"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
(click)="minimize(viewbox)"
|
||||
aria-label="Minimize"
|
||||
shape="minus"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
(click)="close(viewbox)"
|
||||
aria-label="Close"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
#dragHandleCorner
|
||||
[id]="'handle_viewbox_' + viewbox.id"
|
||||
class="dragHandle corner"
|
||||
(mousedown)="focusViewbox(viewbox)"
|
||||
[class.focused]="viewbox.focused"
|
||||
[style.display]="viewbox.minimized ? 'none' : ''"
|
||||
[style.width.px]="viewbox.width"
|
||||
[style.height.px]="viewbox.height"
|
||||
[style.pointer-events]="viewbox.collapsed ? 'none' : ''"
|
||||
[style.box-shadow]="viewbox.collapsed ? 'none' : ''"
|
||||
class="viewbox"
|
||||
cdkDragBoundary=".viewboxes-container"
|
||||
cdkDrag
|
||||
(cdkDragMoved)="dragMove(dragHandleCorner, resizeBox, viewbox, $event)"
|
||||
></span>
|
||||
<form
|
||||
class="d-flex align-items-center clr-justify-content-between clr-flex-wrap table-search-wrapper"
|
||||
[style.display]="viewbox.collapsed ? 'none' : ''"
|
||||
clrForm
|
||||
>
|
||||
<div class="d-flex align-items-center clr-flex-wrap">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
#searchEl
|
||||
(keyup.enter)="searchTable(searchEl, viewbox)"
|
||||
clrInput
|
||||
[type]="viewbox.searchNumeric ? 'number' : 'text'"
|
||||
placeholder="Search"
|
||||
name="search-input"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="!viewbox.searchLoading"
|
||||
(click)="searchTable(searchEl, viewbox)"
|
||||
aria-label="Search"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<span *ngIf="viewbox.searchLoading" class="spinner spinner-inline">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
[(ngModel)]="viewbox.searchNumeric"
|
||||
name="numeric_check"
|
||||
/>
|
||||
<label>Numeric</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</div>
|
||||
|
||||
<div class="d-flex clr-align-items-center">
|
||||
<span *ngIf="viewbox.loadingData" class="spinner spinner-sm"
|
||||
>Loading...</span
|
||||
>
|
||||
<clr-icon
|
||||
*ngIf="!viewbox.loadingData"
|
||||
(click)="reloadTableData(viewbox); searchEl.value = ''"
|
||||
aria-label="Reload data"
|
||||
shape="refresh"
|
||||
class="click-icon"
|
||||
></clr-icon>
|
||||
<a
|
||||
(click)="openFilter(viewbox); searchEl.value = ''"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-lg tooltip-bottom-left ml-5"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="filter"
|
||||
[class.filter-active]="viewbox.filter_pk !== '0'"
|
||||
class="click-icon"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content tooltip-long">{{
|
||||
viewbox.filterText
|
||||
}}</span>
|
||||
</a>
|
||||
<clr-icon
|
||||
(click)="openTableEdit(viewbox)"
|
||||
[class.disabled]="!tableEditExists(viewbox)"
|
||||
aria-label="Edit table"
|
||||
shape="pencil"
|
||||
class="click-icon ml-5"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="content"
|
||||
*ngVar="getViewboxTableIndex(viewbox) as viewboxTableIndex"
|
||||
[style.display]="viewbox.collapsed ? 'none' : ''"
|
||||
[cdkDragFreeDragPosition]="{ x: viewbox.x, y: viewbox.y }"
|
||||
(cdkDragEnded)="viewboxDragEnded($event, viewbox)"
|
||||
tabindex="0"
|
||||
[id]="'viewbox_' + viewbox.id"
|
||||
#resizeBox
|
||||
>
|
||||
<div
|
||||
*ngIf="viewbox.loadingData && viewboxTableIndex < 0"
|
||||
class="hot-spinner my-10-mx-auto text-center"
|
||||
class="drag-handle d-flex clr-justify-content-between clr-align-items-center"
|
||||
cdkDragHandle
|
||||
>
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
<span class="table-title">
|
||||
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
|
||||
</span>
|
||||
<div class="actions">
|
||||
@if (!viewbox.collapsed) {
|
||||
<clr-icon
|
||||
(click)="collapse(viewbox)"
|
||||
aria-label="Collapse"
|
||||
shape="angle top"
|
||||
></clr-icon>
|
||||
}
|
||||
@if (viewbox.collapsed) {
|
||||
<clr-icon
|
||||
(click)="expand(viewbox)"
|
||||
aria-label="Expand"
|
||||
shape="angle down"
|
||||
></clr-icon>
|
||||
}
|
||||
<clr-icon
|
||||
(click)="minimize(viewbox)"
|
||||
aria-label="Minimize"
|
||||
shape="minus"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
(click)="close(viewbox)"
|
||||
aria-label="Close"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
#dragHandleCorner
|
||||
[id]="'handle_viewbox_' + viewbox.id"
|
||||
class="dragHandle corner"
|
||||
cdkDrag
|
||||
(cdkDragMoved)="dragMove(dragHandleCorner, resizeBox, viewbox, $event)"
|
||||
></span>
|
||||
<form
|
||||
class="d-flex align-items-center clr-justify-content-between clr-flex-wrap table-search-wrapper"
|
||||
[style.display]="viewbox.collapsed ? 'none' : ''"
|
||||
clrForm
|
||||
>
|
||||
<div class="d-flex align-items-center clr-flex-wrap">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
#searchEl
|
||||
(keyup.enter)="searchTable(searchEl, viewbox)"
|
||||
clrInput
|
||||
[type]="viewbox.searchNumeric ? 'number' : 'text'"
|
||||
placeholder="Search"
|
||||
name="search-input"
|
||||
/>
|
||||
@if (!viewbox.searchLoading) {
|
||||
<clr-icon
|
||||
(click)="searchTable(searchEl, viewbox)"
|
||||
aria-label="Search"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
}
|
||||
@if (viewbox.searchLoading) {
|
||||
<span class="spinner spinner-inline"> Loading... </span>
|
||||
}
|
||||
</div>
|
||||
<clr-checkbox-container>
|
||||
<clr-checkbox-wrapper>
|
||||
<input
|
||||
type="checkbox"
|
||||
clrCheckbox
|
||||
[(ngModel)]="viewbox.searchNumeric"
|
||||
name="numeric_check"
|
||||
/>
|
||||
<label>Numeric</label>
|
||||
</clr-checkbox-wrapper>
|
||||
</clr-checkbox-container>
|
||||
</div>
|
||||
<div class="d-flex clr-align-items-center">
|
||||
@if (viewbox.loadingData) {
|
||||
<span class="spinner spinner-sm">Loading...</span>
|
||||
}
|
||||
@if (!viewbox.loadingData) {
|
||||
<clr-icon
|
||||
(click)="reloadTableData(viewbox); searchEl.value = ''"
|
||||
aria-label="Reload data"
|
||||
shape="refresh"
|
||||
class="click-icon"
|
||||
></clr-icon>
|
||||
}
|
||||
<a
|
||||
(click)="openFilter(viewbox); searchEl.value = ''"
|
||||
role="tooltip"
|
||||
aria-haspopup="true"
|
||||
class="tooltip tooltip-lg tooltip-bottom-left ml-5"
|
||||
>
|
||||
<clr-icon
|
||||
aria-hidden="true"
|
||||
shape="filter"
|
||||
[class.filter-active]="viewbox.filter_pk !== '0'"
|
||||
class="click-icon"
|
||||
></clr-icon>
|
||||
<span class="tooltip-content tooltip-long">{{
|
||||
viewbox.filterText
|
||||
}}</span>
|
||||
</a>
|
||||
<clr-icon
|
||||
(click)="openTableEdit(viewbox)"
|
||||
[class.disabled]="!tableEditExists(viewbox)"
|
||||
aria-label="Edit table"
|
||||
shape="pencil"
|
||||
class="click-icon ml-5"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="content"
|
||||
*ngVar="getViewboxTableIndex(viewbox) as viewboxTableIndex"
|
||||
[style.display]="viewbox.collapsed ? 'none' : ''"
|
||||
>
|
||||
@if (viewbox.loadingData && viewboxTableIndex < 0) {
|
||||
<div class="hot-spinner my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
}
|
||||
@if (viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)) {
|
||||
<hot-table
|
||||
[settings]="viewboxHotSettings.get(viewbox.id) || {}"
|
||||
[id]="'hotTable_' + viewbox.id"
|
||||
></hot-table>
|
||||
}
|
||||
</div>
|
||||
|
||||
<hot-table
|
||||
*ngIf="viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)"
|
||||
[settings]="viewboxHotSettings.get(viewbox.id) || {}"
|
||||
[id]="'hotTable_' + viewbox.id"
|
||||
></hot-table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<clr-modal
|
||||
@@ -433,11 +437,9 @@
|
||||
<span> #{{ filteringViewbox?.id }} {{ filterLibds }} </span>
|
||||
</h3>
|
||||
<div class="modal-body">
|
||||
<app-query
|
||||
#queryFilter
|
||||
*ngIf="filter"
|
||||
[viewboxId]="filteringViewbox?.id"
|
||||
></app-query>
|
||||
@if (filter) {
|
||||
<app-query #queryFilter [viewboxId]="filteringViewbox?.id"></app-query>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button (click)="resetFilter()" type="button" class="btn btn-sm btn-link">
|
||||
|
||||
@@ -1,140 +1,146 @@
|
||||
<main class="content-area">
|
||||
<div class="clr-col-md-12 card">
|
||||
<div
|
||||
*ngIf="!loaded"
|
||||
class="h-80vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
<span class="spinner" *ngIf="!loaded"> Loading... </span>
|
||||
<div *ngIf="!loaded">
|
||||
<h3>Loading submitted table</h3>
|
||||
@if (!loaded) {
|
||||
<div
|
||||
class="h-80vh d-flex justify-content-center flex-column align-items-center"
|
||||
>
|
||||
@if (!loaded) {
|
||||
<span class="spinner"> Loading... </span>
|
||||
}
|
||||
@if (!loaded) {
|
||||
<div>
|
||||
<h3>Loading submitted table</h3>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loaded">
|
||||
<div class="card-header" ng-if="loaded">
|
||||
<div class="card-title text-center">
|
||||
<h4 class="mt-0">Staged Data</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row" ng-if="loaded">
|
||||
<div class="card-block clr-col-md-6 no-borders">
|
||||
<div class="card-header py-5">
|
||||
<div class="card-title text-center">Basic Submitted Details</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Table Id:</span>
|
||||
<strong>
|
||||
<span> {{ tableDetails?.TABLE_ID }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Base Table:</span>
|
||||
<a (click)="viewerTableScreen()" class="baseTableLink">
|
||||
{{ tableDetails?.BASE_TABLE }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submit Date:</span>
|
||||
<span class="mt-10">
|
||||
{{ tableDetails?.SUBMITTED_ON_DTTM }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Review Status:</span>
|
||||
<span
|
||||
class="mt-10"
|
||||
[ngClass]="{
|
||||
rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED',
|
||||
accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED'
|
||||
}"
|
||||
[attr.aria-label]="
|
||||
'Review status: ' + tableDetails?.REVIEW_STATUS_ID
|
||||
"
|
||||
role="status"
|
||||
>
|
||||
{{ tableDetails?.REVIEW_STATUS_ID }}
|
||||
</span>
|
||||
}
|
||||
@if (loaded) {
|
||||
<div>
|
||||
<div class="card-header" ng-if="loaded">
|
||||
<div class="card-title text-center">
|
||||
<h4 class="mt-0">Staged Data</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-md-6 card-block no-borders">
|
||||
<div class="card-header py-5">
|
||||
<div class="card-title text-center">Actions</div>
|
||||
</div>
|
||||
<div class="mt-20">
|
||||
<div class="row">
|
||||
<button
|
||||
class="btn btn-sm btn-outline text-center mr-5i"
|
||||
(click)="viewerTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="View base table"
|
||||
>
|
||||
View base table
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!(tableDetails?.['ALLOW_RESTORE'] === 'YES')"
|
||||
id="approval-btn"
|
||||
class="btn btn-sm btn-success-outline text-center mr-5i"
|
||||
[disabled]="
|
||||
tableDetails?.REVIEW_STATUS_ID === 'APPROVED' ||
|
||||
tableDetails?.REVIEW_STATUS_ID === 'REJECTED'
|
||||
<div class="clr-row" ng-if="loaded">
|
||||
<div class="card-block clr-col-md-6 no-borders">
|
||||
<div class="card-header py-5">
|
||||
<div class="card-title text-center">Basic Submitted Details</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>Table Id:</span>
|
||||
<strong>
|
||||
<span> {{ tableDetails?.TABLE_ID }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Base Table:</span>
|
||||
<a (click)="viewerTableScreen()" class="baseTableLink">
|
||||
{{ tableDetails?.BASE_TABLE }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span>Submit Date:</span>
|
||||
<span class="mt-10">
|
||||
{{ tableDetails?.SUBMITTED_ON_DTTM }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Review Status:</span>
|
||||
<span
|
||||
class="mt-10"
|
||||
[ngClass]="{
|
||||
rejected: tableDetails?.REVIEW_STATUS_ID === 'REJECTED',
|
||||
accepted: tableDetails?.REVIEW_STATUS_ID === 'APPROVED'
|
||||
}"
|
||||
[attr.aria-label]="
|
||||
'Review status: ' + tableDetails?.REVIEW_STATUS_ID
|
||||
"
|
||||
(click)="approveTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Approve table"
|
||||
role="status"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center mr-5i"
|
||||
(click)="goBack()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Edit base table"
|
||||
>
|
||||
Edit base table
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success text-center mr-5i min-w-0"
|
||||
(click)="download(tableDetails?.TABLE_ID)"
|
||||
aria-label="Download audit file"
|
||||
>
|
||||
<clr-icon shape="download" aria-hidden="true"></clr-icon>
|
||||
</button>
|
||||
|
||||
<clr-tooltip>
|
||||
{{ tableDetails?.REVIEW_STATUS_ID }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-md-6 card-block no-borders">
|
||||
<div class="card-header py-5">
|
||||
<div class="card-title text-center">Actions</div>
|
||||
</div>
|
||||
<div class="mt-20">
|
||||
<div class="row">
|
||||
<button
|
||||
*ngIf="tableDetails?.['ALLOW_RESTORE'] === 'YES'"
|
||||
(click)="revertChanges()"
|
||||
clrTooltipTrigger
|
||||
[clrLoading]="revertingChanges"
|
||||
class="btn btn-sm btn-danger text-center mt-20"
|
||||
aria-label="Revert this and all subsequent changes"
|
||||
class="btn btn-sm btn-outline text-center mr-5i"
|
||||
(click)="viewerTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="View base table"
|
||||
>
|
||||
REVERT
|
||||
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-left"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
<span> Revert this and all subsequent changes </span>
|
||||
</clr-tooltip-content>
|
||||
View base table
|
||||
</button>
|
||||
</clr-tooltip>
|
||||
@if (!(tableDetails?.['ALLOW_RESTORE'] === 'YES')) {
|
||||
<button
|
||||
id="approval-btn"
|
||||
class="btn btn-sm btn-success-outline text-center mr-5i"
|
||||
[disabled]="
|
||||
tableDetails?.REVIEW_STATUS_ID === 'APPROVED' ||
|
||||
tableDetails?.REVIEW_STATUS_ID === 'REJECTED'
|
||||
"
|
||||
(click)="approveTableScreen()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Approve table"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
class="btn btn-sm btn-info-outline text-center mr-5i"
|
||||
(click)="goBack()"
|
||||
[disabled]="revertingChanges"
|
||||
aria-label="Edit base table"
|
||||
>
|
||||
Edit base table
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-success text-center mr-5i min-w-0"
|
||||
(click)="download(tableDetails?.TABLE_ID)"
|
||||
aria-label="Download audit file"
|
||||
>
|
||||
<clr-icon shape="download" aria-hidden="true"></clr-icon>
|
||||
</button>
|
||||
<clr-tooltip>
|
||||
@if (tableDetails?.['ALLOW_RESTORE'] === 'YES') {
|
||||
<button
|
||||
(click)="revertChanges()"
|
||||
clrTooltipTrigger
|
||||
[clrLoading]="revertingChanges"
|
||||
class="btn btn-sm btn-danger text-center mt-20"
|
||||
aria-label="Revert this and all subsequent changes"
|
||||
>
|
||||
REVERT
|
||||
<clr-tooltip-content
|
||||
clrPosition="bottom-left"
|
||||
clrSize="lg"
|
||||
*clrIfOpen
|
||||
>
|
||||
<span> Revert this and all subsequent changes </span>
|
||||
</clr-tooltip-content>
|
||||
</button>
|
||||
}
|
||||
</clr-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<hot-table
|
||||
id="hotTable"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings()"
|
||||
aria-label="Staged data table"
|
||||
>
|
||||
<!--[licenseKey]=null-->
|
||||
</hot-table>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-center"></div>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<hot-table
|
||||
id="hotTable"
|
||||
[data]="hotTable.data"
|
||||
[settings]="hotTableSettings()"
|
||||
aria-label="Staged data table"
|
||||
>
|
||||
<!--[licenseKey]=null-->
|
||||
</hot-table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex justify-content-center"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -104,71 +104,68 @@
|
||||
Free Tier:
|
||||
<span class="dark">{{ licenceInfo?.demo ? 'YES' : 'NO' }}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.viewer_rows_allowed !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Viewer rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.viewer_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.editor_rows_allowed !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Editor rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.editor_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.stage_rows_allowed !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Stage rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.stage_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.history_rows_allowed !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
History rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.history_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.submit_rows_limit !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Submit rows limit:
|
||||
<span class="dark">{{ licenceState.value.submit_rows_limit }}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.tables_in_library_limit !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Tables in library limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.tables_in_library_limit
|
||||
}}</span>
|
||||
</p>
|
||||
<p *ngIf="licenceState.value.viewbox_limit !== Infinity" class="m-0">
|
||||
Viewboxes limit:
|
||||
<span class="dark">{{ licenceState.value.viewbox_limit }}</span>
|
||||
</p>
|
||||
<p
|
||||
*ngIf="licenceState.value.lineage_daily_limit !== Infinity"
|
||||
class="m-0"
|
||||
>
|
||||
Lineage daily limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.lineage_daily_limit
|
||||
}}</span>
|
||||
</p>
|
||||
@if (licenceState.value.viewer_rows_allowed !== Infinity) {
|
||||
<p class="m-0">
|
||||
Viewer rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.viewer_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.editor_rows_allowed !== Infinity) {
|
||||
<p class="m-0">
|
||||
Editor rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.editor_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.stage_rows_allowed !== Infinity) {
|
||||
<p class="m-0">
|
||||
Stage rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.stage_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.history_rows_allowed !== Infinity) {
|
||||
<p class="m-0">
|
||||
History rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.history_rows_allowed
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.submit_rows_limit !== Infinity) {
|
||||
<p class="m-0">
|
||||
Submit rows limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.submit_rows_limit
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.tables_in_library_limit !== Infinity) {
|
||||
<p class="m-0">
|
||||
Tables in library limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.tables_in_library_limit
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.viewbox_limit !== Infinity) {
|
||||
<p class="m-0">
|
||||
Viewboxes limit:
|
||||
<span class="dark">{{ licenceState.value.viewbox_limit }}</span>
|
||||
</p>
|
||||
}
|
||||
@if (licenceState.value.lineage_daily_limit !== Infinity) {
|
||||
<p class="m-0">
|
||||
Lineage daily limit:
|
||||
<span class="dark">{{
|
||||
licenceState.value.lineage_daily_limit
|
||||
}}</span>
|
||||
</p>
|
||||
}
|
||||
<p cds-text="label" class="m-0">
|
||||
Viewboxes:
|
||||
<span class="dark">{{
|
||||
@@ -201,29 +198,28 @@
|
||||
<!-- Keep the logic in case we in future have more then 2 themes (light, dark) -->
|
||||
<!-- -->
|
||||
<!-- <div class="user-action">
|
||||
Keep selected theme after reload
|
||||
Keep selected theme after reload
|
||||
|
||||
<clr-checkbox-wrapper>
|
||||
<clr-checkbox-wrapper>
|
||||
<input [(ngModel)]="settings.persistSelectedTheme" (change)="settingChange($event)" value="persistDarkMode" type="checkbox" clrToggle />
|
||||
</clr-checkbox-wrapper>
|
||||
</div> -->
|
||||
|
||||
<ng-container *ngIf="environmentInfo?.ISADMIN === 1">
|
||||
<div *ngIf="serverType === 'SAS9'" class="admin-action">
|
||||
Refresh Data Lineage
|
||||
|
||||
<button
|
||||
(click)="refreshDataLineage()"
|
||||
[clrLoading]="refreshingDataLineage"
|
||||
class="btn btn-info btn-sm"
|
||||
>
|
||||
REFRESH
|
||||
</button>
|
||||
</div>
|
||||
</clr-checkbox-wrapper>
|
||||
</div> -->
|
||||
|
||||
@if (environmentInfo?.ISADMIN === 1) {
|
||||
@if (serverType === 'SAS9') {
|
||||
<div class="admin-action">
|
||||
Refresh Data Lineage
|
||||
<button
|
||||
(click)="refreshDataLineage()"
|
||||
[clrLoading]="refreshingDataLineage"
|
||||
class="btn btn-info btn-sm"
|
||||
>
|
||||
REFRESH
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<div class="admin-action">
|
||||
Refresh Data Catalog
|
||||
|
||||
<button
|
||||
(click)="refreshDataCatalog()"
|
||||
[clrLoading]="refreshingDataCatalog"
|
||||
@@ -232,10 +228,8 @@
|
||||
REFRESH
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-action">
|
||||
Download Configuration
|
||||
|
||||
<div class="libref-group">
|
||||
<clr-tooltip class="libref-tooltip">
|
||||
<label clrTooltipTrigger class="libref-label">
|
||||
@@ -251,7 +245,6 @@
|
||||
contain this, instead of the original.
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="clr-input libref-input"
|
||||
@@ -269,15 +262,13 @@
|
||||
DOWNLOAD
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-action">
|
||||
Update Licence Key
|
||||
|
||||
<button routerLink="/licensing/update" class="btn btn-info btn-sm">
|
||||
UPDATE
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,210 +1,257 @@
|
||||
<app-sidebar class="sidebar-height">
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="users" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Users"
|
||||
name="input"
|
||||
[(ngModel)]="userSearch"
|
||||
(keyup)="userListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLibTreeInput.value.length > 0"
|
||||
(click)="userSearch = ''; userListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
<ng-container *ngFor="let user of users">
|
||||
<clr-tree-node
|
||||
(click)="userOnClick(user)"
|
||||
*ngIf="!user['hidden']"
|
||||
[class.active]="userInfo?.URI === user.URI"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="user"></clr-icon>
|
||||
{{ user.NAME }}
|
||||
</p>
|
||||
@if (users) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLibTreeInput
|
||||
placeholder="Filter by Users"
|
||||
name="input"
|
||||
[(ngModel)]="userSearch"
|
||||
(keyup)="userListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLibTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLibTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="userSearch = ''; userListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
@for (user of users; track user) {
|
||||
@if (!user['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="userOnClick(user)"
|
||||
[class.active]="userInfo?.URI === user.URI"
|
||||
>
|
||||
<p class="m-0 cursor-pointer list-padding">
|
||||
<clr-icon shape="user"></clr-icon>
|
||||
{{ user.NAME }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
<div class="content-area">
|
||||
<div *ngIf="loading" class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="userData && !loading">
|
||||
<div *ngIf="serverType !== ServerType.SasViya" class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table user-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.NAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.DISPLAYNAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-container *ngIf="serverType === ServerType.Sas9">
|
||||
<tr>
|
||||
<td class="left">
|
||||
Metadata Created: <b>{{ userInfo.METADATACREATED }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
Metadata Updated: <b>{{ userInfo.METADATAUPDATED }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@if (loading) {
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div *ngIf="serverType === ServerType.SasViya && userInfo" class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table user-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.NAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
User ID: <b>{{ userInfo.URI }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
User provider ID: <b>{{ userInfo.PROVIDER }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card user-data">
|
||||
<div *ngIf="serverType === ServerType.Sas9">
|
||||
<h3>EMAILS ({{ userEmailsCount || '0' }})</h3>
|
||||
<h5 *ngIf="userEmailsCount == 0">No Emails Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="userEmailsCount != 0" class="table user-table">
|
||||
<thead>
|
||||
@if (userData && !loading) {
|
||||
<div>
|
||||
@if (serverType !== ServerType.SasViya) {
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table user-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.NAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.DISPLAYNAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<tr>
|
||||
<td class="width-50"><b>EMAIL</b></td>
|
||||
<td class=""><b>TYPE</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let email of userEmails">
|
||||
<td class="">{{ email.EMAIL }}</td>
|
||||
<td class="">{{ email.TYPE }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr *ngIf="serverType === ServerType.Sas9" />
|
||||
<div>
|
||||
<h3>GROUPS ({{ userGroupsCount || '0' }})</h3>
|
||||
<h5 *ngIf="userGroupsCount == 0">No Groups Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="userGroupsCount != 0" class="table user-table">
|
||||
<tbody>
|
||||
<tr *ngFor="let group of userGroups">
|
||||
<td
|
||||
*ngIf="serverType === ServerType.SasViya"
|
||||
[routerLink]="'/view/usernav/groups/' + group.URI"
|
||||
routerLinkActive="active"
|
||||
class=""
|
||||
>
|
||||
{{ group.GROUPNAME }}
|
||||
</td>
|
||||
<ng-container *ngIf="serverType !== ServerType.SasViya">
|
||||
<td
|
||||
[routerLink]="
|
||||
'/view/usernav/groups/' +
|
||||
(group.GROUPURI || group.URI)
|
||||
"
|
||||
routerLinkActive="active"
|
||||
class=""
|
||||
>
|
||||
{{ group.GROUPNAME }}
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr *ngIf="serverType === ServerType.Sas9" />
|
||||
<div *ngIf="serverType === ServerType.Sas9">
|
||||
<h3>ROLES ({{ userRolesCount || '0' }})</h3>
|
||||
<h5 *ngIf="userRolesCount == 0">No Roles Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="userRolesCount != 0" class="table user-table">
|
||||
<tbody>
|
||||
<tr *ngFor="let role of userRoles">
|
||||
<td
|
||||
[routerLink]="
|
||||
'/view/usernav/roles/' + getRoleURI(role.ROLEURI)
|
||||
"
|
||||
>
|
||||
{{ role.ROLENAME }}
|
||||
<td class="left">
|
||||
Metadata Created: <b>{{ userInfo.METADATACREATED }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr *ngIf="serverType === ServerType.Sas9" />
|
||||
<div *ngIf="serverType === ServerType.Sas9">
|
||||
<h3>LOGINS ({{ userLoginsCount || '0' }})</h3>
|
||||
<h5 *ngIf="userLoginsCount == 0">No Logins Present</h5>
|
||||
<div class="table-container">
|
||||
<table *ngIf="userLoginsCount != 0" class="table user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-50"><b>DOMAIN</b></td>
|
||||
<td><b>USER ID</b></td>
|
||||
<td class="left">
|
||||
Metadata Updated: <b>{{ userInfo.METADATAUPDATED }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let login of userLogins">
|
||||
<td class="">{{ login.DOMAIN }}</td>
|
||||
<td class="">{{ login.USERID }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (serverType === ServerType.SasViya && userInfo) {
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<table class="table user-info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="left">
|
||||
<p class="user-info-text">
|
||||
<b>{{ userInfo.NAME }}</b>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
User ID: <b>{{ userInfo.URI }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left">
|
||||
User provider ID: <b>{{ userInfo.PROVIDER }}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-8">
|
||||
<div class="card user-data">
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<div>
|
||||
<h3>EMAILS ({{ userEmailsCount || '0' }})</h3>
|
||||
@if (userEmailsCount == 0) {
|
||||
<h5>No Emails Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (userEmailsCount != 0) {
|
||||
<table class="table user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-50"><b>EMAIL</b></td>
|
||||
<td class=""><b>TYPE</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (email of userEmails; track email) {
|
||||
<tr>
|
||||
<td class="">{{ email.EMAIL }}</td>
|
||||
<td class="">{{ email.TYPE }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<hr />
|
||||
}
|
||||
<div>
|
||||
<h3>GROUPS ({{ userGroupsCount || '0' }})</h3>
|
||||
@if (userGroupsCount == 0) {
|
||||
<h5>No Groups Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (userGroupsCount != 0) {
|
||||
<table class="table user-table">
|
||||
<tbody>
|
||||
@for (group of userGroups; track group) {
|
||||
<tr>
|
||||
@if (serverType === ServerType.SasViya) {
|
||||
<td
|
||||
[routerLink]="'/view/usernav/groups/' + group.URI"
|
||||
routerLinkActive="active"
|
||||
class=""
|
||||
>
|
||||
{{ group.GROUPNAME }}
|
||||
</td>
|
||||
}
|
||||
@if (serverType !== ServerType.SasViya) {
|
||||
<td
|
||||
[routerLink]="
|
||||
'/view/usernav/groups/' +
|
||||
(group.GROUPURI || group.URI)
|
||||
"
|
||||
routerLinkActive="active"
|
||||
class=""
|
||||
>
|
||||
{{ group.GROUPNAME }}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<hr />
|
||||
}
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<div>
|
||||
<h3>ROLES ({{ userRolesCount || '0' }})</h3>
|
||||
@if (userRolesCount == 0) {
|
||||
<h5>No Roles Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (userRolesCount != 0) {
|
||||
<table class="table user-table">
|
||||
<tbody>
|
||||
@for (role of userRoles; track role) {
|
||||
<tr>
|
||||
<td
|
||||
[routerLink]="
|
||||
'/view/usernav/roles/' +
|
||||
getRoleURI(role.ROLEURI)
|
||||
"
|
||||
>
|
||||
{{ role.ROLENAME }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<hr />
|
||||
}
|
||||
@if (serverType === ServerType.Sas9) {
|
||||
<div>
|
||||
<h3>LOGINS ({{ userLoginsCount || '0' }})</h3>
|
||||
@if (userLoginsCount == 0) {
|
||||
<h5>No Logins Present</h5>
|
||||
}
|
||||
<div class="table-container">
|
||||
@if (userLoginsCount != 0) {
|
||||
<table class="table user-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="width-50"><b>DOMAIN</b></td>
|
||||
<td><b>USER ID</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (login of userLogins; track login) {
|
||||
<tr>
|
||||
<td class="">{{ login.DOMAIN }}</td>
|
||||
<td class="">{{ login.USERID }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,107 +8,113 @@
|
||||
[(ngModel)]="collection"
|
||||
(change)="updateSelectedCollection()"
|
||||
>
|
||||
<optgroup
|
||||
*ngFor="let collection of collections | keyvalue"
|
||||
[label]="collection.key"
|
||||
>
|
||||
<option
|
||||
*ngFor="let endpoint of collections[collection.key] | keyvalue"
|
||||
value="{{ endpoint.value }}"
|
||||
>
|
||||
{{ endpoint.key }}
|
||||
</option>
|
||||
</optgroup>
|
||||
@for (collection of collections | keyvalue; track collection) {
|
||||
<optgroup [label]="collection.key">
|
||||
@for (
|
||||
endpoint of collections[collection.key] | keyvalue;
|
||||
track endpoint
|
||||
) {
|
||||
<option value="{{ endpoint.value }}">
|
||||
{{ endpoint.key }}
|
||||
</option>
|
||||
}
|
||||
</optgroup>
|
||||
}
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
|
||||
<p
|
||||
*ngIf="endpointLinks.length < 1 && !collectionLoading"
|
||||
class="w-100 text-center"
|
||||
>
|
||||
Select collection to start
|
||||
</p>
|
||||
@if (endpointLinks.length < 1 && !collectionLoading) {
|
||||
<p class="w-100 text-center">Select collection to start</p>
|
||||
}
|
||||
|
||||
<div class="d-flex clr-justify-content-center">
|
||||
<clr-spinner *ngIf="collectionLoading" clrMedium></clr-spinner>
|
||||
@if (collectionLoading) {
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
}
|
||||
</div>
|
||||
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="endpointLinks.length > 0" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLinksTreeInput
|
||||
placeholder="Search links"
|
||||
name="input"
|
||||
[(ngModel)]="linksSearch"
|
||||
(keyup)="linksListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchLinksTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchLinksTreeInput.value.length > 0"
|
||||
(click)="linksSearch = ''; linksListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
<ng-container *ngFor="let link of endpointLinks">
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, link, endpointLinks)"
|
||||
*ngIf="!link['hidden']"
|
||||
[(clrExpanded)]="link['expanded']"
|
||||
[clrLoading]="!!link['loadingTables'] && !link.tables"
|
||||
>
|
||||
<p (click)="linkOnClick(link)" class="m-0 cursor-pointer">
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ link.method }} {{ link.rel }}
|
||||
</p>
|
||||
@if (endpointLinks.length > 0) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
appStealFocus
|
||||
clrInput
|
||||
#searchLinksTreeInput
|
||||
placeholder="Search links"
|
||||
name="input"
|
||||
[(ngModel)]="linksSearch"
|
||||
(keyup)="linksListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchLinksTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchLinksTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="linksSearch = ''; linksListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
@for (link of endpointLinks; track link) {
|
||||
@if (!link['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, link, endpointLinks)"
|
||||
[(clrExpanded)]="link['expanded']"
|
||||
[clrLoading]="!!link['loadingTables'] && !link.tables"
|
||||
>
|
||||
<p (click)="linkOnClick(link)" class="m-0 cursor-pointer">
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ link.method }} {{ link.rel }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
|
||||
<clr-tree>
|
||||
<clr-tree-node *ngIf="endpointItems.length > 0" class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
clrInput
|
||||
#searchItemsTreeInput
|
||||
placeholder="Search items"
|
||||
name="input"
|
||||
[(ngModel)]="itemsSearch"
|
||||
(keyup)="itemsListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchItemsTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchItemsTreeInput.value.length > 0"
|
||||
(click)="itemsSearch = ''; itemsListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
<ng-container *ngFor="let item of endpointItems">
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, item, endpointItems)"
|
||||
*ngIf="!item['hidden']"
|
||||
[(clrExpanded)]="item['expanded']"
|
||||
[clrLoading]="!!item['loadingTables'] && !item.tables"
|
||||
>
|
||||
<p (click)="itemOnClick(item)" class="m-0 cursor-pointer">
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ item.name || item.id }}
|
||||
</p>
|
||||
@if (endpointItems.length > 0) {
|
||||
<clr-tree-node class="search-node">
|
||||
<div class="tree-search-wrapper">
|
||||
<input
|
||||
clrInput
|
||||
#searchItemsTreeInput
|
||||
placeholder="Search items"
|
||||
name="input"
|
||||
[(ngModel)]="itemsSearch"
|
||||
(keyup)="itemsListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
@if (searchItemsTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchItemsTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="itemsSearch = ''; itemsListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
@for (item of endpointItems; track item) {
|
||||
@if (!item['hidden']) {
|
||||
<clr-tree-node
|
||||
(click)="treeNodeClicked($event, item, endpointItems)"
|
||||
[(clrExpanded)]="item['expanded']"
|
||||
[clrLoading]="!!item['loadingTables'] && !item.tables"
|
||||
>
|
||||
<p (click)="itemOnClick(item)" class="m-0 cursor-pointer">
|
||||
<clr-icon shape="block"></clr-icon>
|
||||
{{ item.name || item.id }}
|
||||
</p>
|
||||
</clr-tree-node>
|
||||
}
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
@@ -132,12 +138,14 @@
|
||||
|
||||
<h4 class="text-center m-0">
|
||||
API Explorer
|
||||
<clr-spinner *ngIf="endpointLoading" clrSmall></clr-spinner>
|
||||
@if (endpointLoading) {
|
||||
<clr-spinner clrSmall></clr-spinner>
|
||||
}
|
||||
</h4>
|
||||
</div>
|
||||
<hr class="w-100 light" />
|
||||
|
||||
<ng-container *ngIf="collection.length > 0">
|
||||
@if (collection.length > 0) {
|
||||
<div class="d-flex clr-align-items-center clr-justify-content-between">
|
||||
<span class="d-flex clr-align-items-center">
|
||||
GET
|
||||
@@ -154,7 +162,6 @@
|
||||
<clr-icon shape="copy"></clr-icon>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div class="d-flex clr-align-items-center query-params">
|
||||
<clr-input-container class="m-0">
|
||||
<label>Start</label>
|
||||
@@ -187,13 +194,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<pre class="json-area">
|
||||
<ng-container *ngIf="endpointJson">
|
||||
<ngx-json-viewer *ngIf="!rawJson" class="d-block" [json]="endpointJson" [expanded]="false" (segmentLinkClicked)="onJsonLinkClick($event)"></ngx-json-viewer>
|
||||
<ng-container *ngIf="rawJson">
|
||||
@if (endpointJson) {
|
||||
@if (!rawJson) {
|
||||
<ngx-json-viewer class="d-block" [json]="endpointJson" [expanded]="false" (segmentLinkClicked)="onJsonLinkClick($event)"></ngx-json-viewer>
|
||||
}
|
||||
@if (rawJson) {
|
||||
{{ endpointJson | prettyjson }}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</pre>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<app-sidebar>
|
||||
<div *ngIf="xlmapsLoading" class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
@if (xlmapsLoading) {
|
||||
<div class="my-10-mx-auto text-center">
|
||||
<clr-spinner clrMedium></clr-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
<clr-tree>
|
||||
<clr-tree-node class="search-node">
|
||||
@@ -16,19 +18,19 @@
|
||||
(keyup)="xlmapListOnFilter()"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<clr-icon
|
||||
*ngIf="searchXLMapTreeInput.value.length < 1"
|
||||
shape="search"
|
||||
></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="searchXLMapTreeInput.value.length > 0"
|
||||
(click)="searchString = ''; xlmapListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
@if (searchXLMapTreeInput.value.length < 1) {
|
||||
<clr-icon shape="search"></clr-icon>
|
||||
}
|
||||
@if (searchXLMapTreeInput.value.length > 0) {
|
||||
<clr-icon
|
||||
(click)="searchString = ''; xlmapListOnFilter()"
|
||||
shape="times"
|
||||
></clr-icon>
|
||||
}
|
||||
</div>
|
||||
</clr-tree-node>
|
||||
|
||||
<ng-container *ngFor="let xlmap of xlmaps">
|
||||
@for (xlmap of xlmaps; track xlmap) {
|
||||
<clr-tree-node>
|
||||
<button
|
||||
(click)="xlmapOnClick(xlmap)"
|
||||
@@ -39,99 +41,99 @@
|
||||
{{ xlmap.id }}
|
||||
</button>
|
||||
</clr-tree-node>
|
||||
</ng-container>
|
||||
}
|
||||
</clr-tree>
|
||||
</app-sidebar>
|
||||
|
||||
<div class="content-area">
|
||||
<div *ngIf="!selectedXLMap" class="no-table-selected">
|
||||
<img
|
||||
src="images/select-table.png"
|
||||
class="select-table-icon"
|
||||
alt="select table icon"
|
||||
/>
|
||||
<p
|
||||
*ngIf="xlmaps.length > 0"
|
||||
class="text-center color-gray mt-10"
|
||||
cds-text="section"
|
||||
@if (!selectedXLMap) {
|
||||
<div class="no-table-selected">
|
||||
<img
|
||||
src="images/select-table.png"
|
||||
class="select-table-icon"
|
||||
alt="select table icon"
|
||||
/>
|
||||
@if (xlmaps.length > 0) {
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
Please select a map
|
||||
</p>
|
||||
}
|
||||
@if (xlmaps.length < 1) {
|
||||
<p class="text-center color-gray mt-10" cds-text="section">
|
||||
No excel map is found
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoading) {
|
||||
<div class="loadingSpinner">
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div>
|
||||
<h4>{{ isLoadingDesc }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLoading && selectedXLMap) {
|
||||
<div
|
||||
appDragNdrop
|
||||
(fileDraggedOver)="onShowUploadModal()"
|
||||
class="card h-100 d-flex clr-flex-column"
|
||||
>
|
||||
Please select a map
|
||||
</p>
|
||||
<p
|
||||
*ngIf="xlmaps.length < 1"
|
||||
class="text-center color-gray mt-10"
|
||||
cds-text="section"
|
||||
>
|
||||
No excel map is found
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="loadingSpinner" *ngIf="isLoading">
|
||||
<span class="spinner"> Loading... </span>
|
||||
<div>
|
||||
<h4>{{ isLoadingDesc }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
appDragNdrop
|
||||
(fileDraggedOver)="onShowUploadModal()"
|
||||
class="card h-100 d-flex clr-flex-column"
|
||||
*ngIf="!isLoading && selectedXLMap"
|
||||
>
|
||||
<clr-tabs>
|
||||
<clr-tab>
|
||||
<button clrTabLink (click)="selectedTab = TabsEnum.Rules">Rules</button>
|
||||
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Rules">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button clrTabLink (click)="selectedTab = TabsEnum.Data">Data</button>
|
||||
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Data">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
|
||||
<ng-container *ngTemplateOutlet="actionButtons"></ng-container>
|
||||
|
||||
<div class="clr-row m-0 mb-10-i viewerTitle">
|
||||
<h3 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
{{ selectedXLMap.id }}
|
||||
</h3>
|
||||
<i class="d-flex clr-col-12 clr-justify-content-center mt-5-i">{{
|
||||
selectedXLMap.description
|
||||
}}</i>
|
||||
<p class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
Rules Source:
|
||||
<a
|
||||
cds-text="labelLink"
|
||||
class="ml-10"
|
||||
[routerLink]="'/view/data/' + rulesSource"
|
||||
<clr-tabs>
|
||||
<clr-tab>
|
||||
<button clrTabLink (click)="selectedTab = TabsEnum.Rules">
|
||||
Rules
|
||||
</button>
|
||||
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Rules">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
<clr-tab>
|
||||
<button clrTabLink (click)="selectedTab = TabsEnum.Data">Data</button>
|
||||
<clr-tab-content *clrIfActive="selectedTab === TabsEnum.Data">
|
||||
</clr-tab-content>
|
||||
</clr-tab>
|
||||
</clr-tabs>
|
||||
<ng-container *ngTemplateOutlet="actionButtons"></ng-container>
|
||||
<div class="clr-row m-0 mb-10-i viewerTitle">
|
||||
<h3 class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
{{ selectedXLMap.id }}
|
||||
</h3>
|
||||
<i class="d-flex clr-col-12 clr-justify-content-center mt-5-i">{{
|
||||
selectedXLMap.description
|
||||
}}</i>
|
||||
<p class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
Rules Source:
|
||||
<a
|
||||
cds-text="labelLink"
|
||||
class="ml-10"
|
||||
[routerLink]="'/view/data/' + rulesSource"
|
||||
>
|
||||
{{ rulesSource }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
Target dataset:
|
||||
<a
|
||||
cds-text="labelLink"
|
||||
class="ml-10"
|
||||
[routerLink]="'/view/data/' + selectedXLMap.targetDS"
|
||||
>
|
||||
{{ selectedXLMap.targetDS }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="clr-flex-1">
|
||||
<hot-table
|
||||
id="hot-table"
|
||||
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
|
||||
[settings]="hotTableSettings()"
|
||||
>
|
||||
{{ rulesSource }}
|
||||
</a>
|
||||
</p>
|
||||
<p class="d-flex clr-col-12 clr-justify-content-center mt-5-i">
|
||||
Target dataset:
|
||||
<a
|
||||
cds-text="labelLink"
|
||||
class="ml-10"
|
||||
[routerLink]="'/view/data/' + selectedXLMap.targetDS"
|
||||
>
|
||||
{{ selectedXLMap.targetDS }}
|
||||
</a>
|
||||
</p>
|
||||
</hot-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clr-flex-1">
|
||||
<hot-table
|
||||
id="hot-table"
|
||||
[data]="selectedTab === TabsEnum.Rules ? xlmapRules : xlData"
|
||||
[settings]="hotTableSettings()"
|
||||
>
|
||||
</hot-table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<clr-modal
|
||||
appFileDrop
|
||||
@@ -207,44 +209,41 @@
|
||||
|
||||
<ng-template #actionButtons>
|
||||
<div class="clr-row m-0 clr-justify-content-center">
|
||||
<div
|
||||
*ngIf="status === StatusEnum.ReadyToUpload"
|
||||
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success btn-block mr-0"
|
||||
(click)="onShowUploadModal()"
|
||||
>
|
||||
<clr-icon shape="upload"></clr-icon>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="status === StatusEnum.ReadyToSubmit"
|
||||
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success btn-block mr-0"
|
||||
(click)="submitExcel()"
|
||||
>
|
||||
<clr-icon shape="upload"></clr-icon>
|
||||
<span>Submit</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="status === StatusEnum.ReadyToSubmit"
|
||||
class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger btn-block mr-0"
|
||||
(click)="discardExtractedData()"
|
||||
>
|
||||
<clr-icon shape="times"></clr-icon>
|
||||
<span>Discard</span>
|
||||
</button>
|
||||
</div>
|
||||
@if (status === StatusEnum.ReadyToUpload) {
|
||||
<div class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success btn-block mr-0"
|
||||
(click)="onShowUploadModal()"
|
||||
>
|
||||
<clr-icon shape="upload"></clr-icon>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (status === StatusEnum.ReadyToSubmit) {
|
||||
<div class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success btn-block mr-0"
|
||||
(click)="submitExcel()"
|
||||
>
|
||||
<clr-icon shape="upload"></clr-icon>
|
||||
<span>Submit</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (status === StatusEnum.ReadyToSubmit) {
|
||||
<div class="d-flex clr-justify-content-center clr-col-12 clr-col-lg-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-danger btn-block mr-0"
|
||||
(click)="discardExtractedData()"
|
||||
>
|
||||
<clr-icon shape="times"></clr-icon>
|
||||
<span>Discard</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Type declarations for the untyped `browserify-cipher` package.
|
||||
* Only the cipher functions used by `src/crypto-shim.ts` are declared.
|
||||
*/
|
||||
declare module 'browserify-cipher' {
|
||||
export interface Cipheriv {
|
||||
update(data: unknown): unknown
|
||||
final(): unknown
|
||||
setAutoPadding(autoPadding?: boolean): void
|
||||
}
|
||||
|
||||
export function createCipheriv(
|
||||
algorithm: string,
|
||||
key: unknown,
|
||||
iv: unknown
|
||||
): Cipheriv
|
||||
|
||||
export function createDecipheriv(
|
||||
algorithm: string,
|
||||
key: unknown,
|
||||
iv: unknown
|
||||
): Cipheriv
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Minimal browser shim for the node `crypto` module.
|
||||
*
|
||||
* Replaces `crypto-browserify`, which pulls in `elliptic` (via
|
||||
* `browserify-sign` and `create-ecdh`) - a package with open low-severity
|
||||
* advisories and no fixed release. None of the signing/ECDH functionality
|
||||
* is needed here: `@sheet/crypto` (see tsconfig `paths` mapping for
|
||||
* "crypto") only uses hashing, HMAC, AES ciphers and random bytes.
|
||||
*/
|
||||
import createHash from 'create-hash'
|
||||
import createHmac from 'create-hmac'
|
||||
import { createCipheriv, createDecipheriv } from 'browserify-cipher'
|
||||
import randomBytes from 'randombytes'
|
||||
|
||||
const HASHES = [
|
||||
'md4',
|
||||
'md5',
|
||||
'ripemd160',
|
||||
'sha1',
|
||||
'sha224',
|
||||
'sha256',
|
||||
'sha384',
|
||||
'sha512'
|
||||
]
|
||||
|
||||
export function getHashes(): string[] {
|
||||
return [...HASHES]
|
||||
}
|
||||
|
||||
export { createHash, createHmac, createCipheriv, createDecipheriv, randomBytes }
|
||||
+7
-10
@@ -15,21 +15,18 @@
|
||||
-->
|
||||
|
||||
<!-- meta tags -->
|
||||
<!--
|
||||
NOTE: Data Controller must run entirely offline / on-prem. Never reference
|
||||
external assets (fonts, images, scripts, styles, CDN links, or remote
|
||||
og:/itemprop URLs) here or anywhere in the built product. All assets must
|
||||
be bundled and served locally.
|
||||
-->
|
||||
<meta name="description" content="Capture, Review, and Approve" />
|
||||
<meta itemprop="name" content="Data Controller for SAS®" />
|
||||
<meta itemprop="description" content="Capture, Review, and Approve" />
|
||||
<meta
|
||||
itemprop="image"
|
||||
content="https://docs.datacontroller.io/img/dc_bg_Asset-5@2x.png"
|
||||
/>
|
||||
<meta property="og:url" content="http://demo.datacontroller.io" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Data Controller for SAS®" />
|
||||
<meta property="og:description" content="Capture, Review, and Approve" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://docs.datacontroller.io/img/dc_bg_Asset-5@2x.png"
|
||||
/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
@@ -54,7 +51,7 @@
|
||||
|
||||
<sasjs
|
||||
serverUrl=""
|
||||
appLoc="/Public/app/devtest"
|
||||
appLoc="/Public/app/dc"
|
||||
serverType="SASJS"
|
||||
loginMechanism="Redirected"
|
||||
debug="false"
|
||||
|
||||
+971
-926
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,6 @@
|
||||
"outDir": "./app",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/polyfills.ts", "src/main.ts", "src/app/app.d.ts"],
|
||||
"files": ["src/polyfills.ts", "src/main.ts", "src/app/app.d.ts", "src/crypto-shim.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -9,16 +9,15 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"lib": ["ES2022", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"module": "ES2022",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2022",
|
||||
"paths": {
|
||||
"crypto": ["./node_modules/crypto-browserify"],
|
||||
"crypto": ["./src/crypto-shim"],
|
||||
"stream": ["./node_modules/stream-browserify"],
|
||||
"assert": ["./node_modules/assert"],
|
||||
"http": ["./node_modules/stream-http"],
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"files": ["src/polyfills.ts"],
|
||||
"files": ["src/polyfills.ts", "src/crypto-shim.ts"],
|
||||
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"fromjs": [
|
||||
{
|
||||
"ADMIN": "AllUsers",
|
||||
"DCPATH": "/opt/data/DataController"
|
||||
"DCPATH": "/opt/data/DataController",
|
||||
"OUTFMT": "JSON"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"fromjs": [
|
||||
{
|
||||
"ADMIN": "AllUsers",
|
||||
"DCPATH": "/tmp/dcdata"
|
||||
"DCPATH": "/tmp/dcdata",
|
||||
"OUTFMT": "JSON"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +44,9 @@ const data = {
|
||||
cols: [
|
||||
{
|
||||
NAME: "PRIMARY_KEY_FIELD",
|
||||
VARNUM: 1,
|
||||
LABEL: "PRIMARY_KEY_FIELD",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -55,10 +54,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_BESTNUM",
|
||||
VARNUM: 9,
|
||||
LABEL: "SOME_BESTNUM",
|
||||
FMTNAME: "BEST",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -66,10 +64,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_CHAR",
|
||||
VARNUM: 2,
|
||||
LABEL: "SOME_CHAR",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -77,7 +74,6 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DATE",
|
||||
VARNUM: 5,
|
||||
LABEL: "SOME_DATE",
|
||||
FMTNAME: "DATE",
|
||||
DDTYPE: "DATE",
|
||||
@@ -88,7 +84,6 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DATETIME",
|
||||
VARNUM: 6,
|
||||
LABEL: "SOME_DATETIME",
|
||||
FMTNAME: "DATETIME",
|
||||
DDTYPE: "DATETIME",
|
||||
@@ -99,10 +94,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DROPDOWN",
|
||||
VARNUM: 3,
|
||||
LABEL: "SOME_DROPDOWN",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -110,10 +104,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_HARDSELECT",
|
||||
VARNUM: 10,
|
||||
LABEL: "SOME_HARDSELECT",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -121,10 +114,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_NUM",
|
||||
VARNUM: 4,
|
||||
LABEL: "SOME_NUM",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -132,10 +124,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_SHORTNUM",
|
||||
VARNUM: 8,
|
||||
LABEL: "SOME_SHORTNUM",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -143,7 +134,6 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_TIME",
|
||||
VARNUM: 7,
|
||||
LABEL: "SOME_TIME",
|
||||
FMTNAME: "TIME",
|
||||
DDTYPE: "TIME",
|
||||
@@ -154,10 +144,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "READONLY_COL",
|
||||
VARNUM: 11,
|
||||
LABEL: "READONLY_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Read-only: default value inserted on add-row, not editable",
|
||||
@@ -165,10 +154,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "HIDDEN_COL",
|
||||
VARNUM: 12,
|
||||
LABEL: "HIDDEN_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Hidden: invisible in grid but submitted; default on add-row",
|
||||
@@ -176,10 +164,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "ROUND_COL",
|
||||
VARNUM: 13,
|
||||
LABEL: "ROUND_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Round: edited values rounded Excel-style to 2 decimals",
|
||||
@@ -187,10 +174,9 @@ const data = {
|
||||
},
|
||||
{
|
||||
NAME: "NUMFMT_COL",
|
||||
VARNUM: 14,
|
||||
LABEL: "NUMFMT_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Number format: displayed as EUR currency (value unchanged)",
|
||||
|
||||
@@ -9,37 +9,33 @@ _webout=`{"SYSDATE" : "26SEP22"
|
||||
{
|
||||
"NAME": "PRIMARY_KEY_FIELD",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 1,
|
||||
"LABEL": "PRIMARY_KEY_FIELD",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_BESTNUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 9,
|
||||
"LABEL": "SOME_BESTNUM",
|
||||
"FMTNAME": "BEST",
|
||||
"FORMAT": "BEST.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_CHAR",
|
||||
"LENGTH": 32767,
|
||||
"VARNUM": 2,
|
||||
"LABEL": "SOME_CHAR",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32767.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_DATE",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 5,
|
||||
"LABEL": "SOME_DATE",
|
||||
"FMTNAME": "DATE",
|
||||
"FORMAT": "DATE9.",
|
||||
@@ -49,7 +45,6 @@ _webout=`{"SYSDATE" : "26SEP22"
|
||||
{
|
||||
"NAME": "SOME_DATETIME",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 6,
|
||||
"LABEL": "SOME_DATETIME",
|
||||
"FMTNAME": "DATETIME",
|
||||
"FORMAT": "DATETIME19.",
|
||||
@@ -59,37 +54,33 @@ _webout=`{"SYSDATE" : "26SEP22"
|
||||
{
|
||||
"NAME": "SOME_DROPDOWN",
|
||||
"LENGTH": 128,
|
||||
"VARNUM": 3,
|
||||
"LABEL": "SOME_DROPDOWN",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$128.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_NUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 4,
|
||||
"LABEL": "SOME_NUM",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_SHORTNUM",
|
||||
"LENGTH": 4,
|
||||
"VARNUM": 8,
|
||||
"LABEL": "SOME_SHORTNUM",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "4.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_TIME",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 7,
|
||||
"LABEL": "SOME_TIME",
|
||||
"FMTNAME": "TIME",
|
||||
"FORMAT": "TIME8.",
|
||||
|
||||
@@ -50,7 +50,9 @@ function makeRows(n) {
|
||||
READONLY_COL: "Readonly default",
|
||||
HIDDEN_COL: "Hidden default",
|
||||
ROUND_COL: Number((i + 1 + i / 7).toFixed(5)), // rounds on edit
|
||||
NUMFMT_COL: 1000 + i * 12.5 // shown as EUR
|
||||
NUMFMT_COL: 1000 + i * 12.5, // shown as EUR
|
||||
REGEX_HARD_COL: "user@example.com", // HARDREGEX: email — starts valid
|
||||
REGEX_SOFT_COL: "SW1A 1AA" // SOFTREGEX: UK postcode — starts valid
|
||||
})
|
||||
}
|
||||
return rows
|
||||
@@ -66,10 +68,9 @@ let webouts = {
|
||||
cols: [
|
||||
{
|
||||
NAME: "PRIMARY_KEY_FIELD",
|
||||
VARNUM: 1,
|
||||
LABEL: "PRIMARY_KEY_FIELD",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -78,10 +79,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_BESTNUM",
|
||||
VARNUM: 9,
|
||||
LABEL: "SOME_BESTNUM",
|
||||
FMTNAME: "BEST",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -90,10 +90,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_CHAR",
|
||||
VARNUM: 2,
|
||||
LABEL: "Some Character Column",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -102,7 +101,6 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DATE",
|
||||
VARNUM: 5,
|
||||
LABEL: "Some Date",
|
||||
FMTNAME: "DATE",
|
||||
DDTYPE: "DATE",
|
||||
@@ -114,7 +112,6 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DATETIME",
|
||||
VARNUM: 6,
|
||||
LABEL: "SOME_DATETIME",
|
||||
FMTNAME: "DATETIME",
|
||||
DDTYPE: "DATETIME",
|
||||
@@ -126,10 +123,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_DROPDOWN",
|
||||
VARNUM: 3,
|
||||
LABEL: "SOME_DROPDOWN",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -138,10 +134,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_HARDSELECT",
|
||||
VARNUM: 10,
|
||||
LABEL: "SOME_HARDSELECT",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -150,10 +145,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_NUM",
|
||||
VARNUM: 4,
|
||||
LABEL: "",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -162,10 +156,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_SHORTNUM",
|
||||
VARNUM: 8,
|
||||
LABEL: "SOME_SHORTNUM",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -174,7 +167,6 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SOME_TIME",
|
||||
VARNUM: 7,
|
||||
LABEL: "SOME_TIME",
|
||||
FMTNAME: "TIME",
|
||||
DDTYPE: "TIME",
|
||||
@@ -186,10 +178,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "READONLY_COL",
|
||||
VARNUM: 11,
|
||||
LABEL: "READONLY_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Read-only: default value inserted on add-row, not editable",
|
||||
@@ -198,10 +189,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "HIDDEN_COL",
|
||||
VARNUM: 12,
|
||||
LABEL: "HIDDEN_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Hidden: invisible in grid but submitted; default on add-row",
|
||||
@@ -210,10 +200,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "ROUND_COL",
|
||||
VARNUM: 13,
|
||||
LABEL: "ROUND_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Round: edited values rounded Excel-style to 2 decimals",
|
||||
@@ -222,15 +211,36 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "NUMFMT_COL",
|
||||
VARNUM: 14,
|
||||
LABEL: "NUMFMT_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "Number format: displayed as EUR currency (value unchanged)",
|
||||
LONGDESC: "",
|
||||
COLTYPE: "{\"data\":\"NUMFMT_COL\",\"type\":\"numeric\",\"format\":\"0\"}"
|
||||
},
|
||||
{
|
||||
NAME: "REGEX_HARD_COL",
|
||||
LABEL: "REGEX_HARD_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "HARDREGEX: must be a valid email address or submission is blocked",
|
||||
LONGDESC: "",
|
||||
COLTYPE: "{\"data\":\"REGEX_HARD_COL\"}"
|
||||
},
|
||||
{
|
||||
NAME: "REGEX_SOFT_COL",
|
||||
LABEL: "REGEX_SOFT_COL",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "SOFTREGEX: should be a valid UK postcode, shown as a yellow warning if not, but can still submit",
|
||||
LONGDESC: "",
|
||||
COLTYPE: "{\"data\":\"REGEX_SOFT_COL\"}"
|
||||
}
|
||||
],
|
||||
dqdata: [
|
||||
@@ -254,7 +264,9 @@ let webouts = {
|
||||
{ BASE_COL: "READONLY_COL", RULE_TYPE: "READONLY", RULE_VALUE: "Readonly default" },
|
||||
{ BASE_COL: "HIDDEN_COL", RULE_TYPE: "HIDDEN", RULE_VALUE: "Hidden default" },
|
||||
{ BASE_COL: "ROUND_COL", RULE_TYPE: "ROUND", RULE_VALUE: "2" },
|
||||
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}' }
|
||||
{ BASE_COL: "NUMFMT_COL", RULE_TYPE: "NUMBER_FORMAT", RULE_VALUE: '{"style":"currency","currency":"EUR"}' },
|
||||
{ BASE_COL: "REGEX_HARD_COL", RULE_TYPE: "HARDREGEX", RULE_VALUE: "/[\\w.]+@[\\w]+\\.[a-z]{2,}/" },
|
||||
{ BASE_COL: "REGEX_SOFT_COL", RULE_TYPE: "SOFTREGEX", RULE_VALUE: "/[A-Z]{1,2}\\d{1,2}[A-Z]?\\s?\\d[A-Z]{2}/" }
|
||||
],
|
||||
dsmeta: [
|
||||
{ ODS_TABLE: "ATTRIBUTES", NAME: "Data Set Name", VALUE: "DC996664.MPE_X_TEST" },
|
||||
@@ -305,7 +317,9 @@ let webouts = {
|
||||
{ NAME: "readonly_col", MAXLEN: 16 },
|
||||
{ NAME: "hidden_col", MAXLEN: 14 },
|
||||
{ NAME: "round_col", MAXLEN: 8 },
|
||||
{ NAME: "numfmt_col", MAXLEN: 8 }
|
||||
{ NAME: "numfmt_col", MAXLEN: 8 },
|
||||
{ NAME: "regex_hard_col", MAXLEN: 128 },
|
||||
{ NAME: "regex_soft_col", MAXLEN: 128 }
|
||||
],
|
||||
query: [],
|
||||
sasdata: makeRows(100),
|
||||
@@ -325,12 +339,14 @@ let webouts = {
|
||||
READONLY_COL: { format: "$200.", label: "READONLY_COL", length: "200", type: "char" },
|
||||
HIDDEN_COL: { format: "$200.", label: "HIDDEN_COL", length: "200", type: "char" },
|
||||
ROUND_COL: { format: "best.", label: "ROUND_COL", length: "8", type: "num" },
|
||||
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" }
|
||||
NUMFMT_COL: { format: "best.", label: "NUMFMT_COL", length: "8", type: "num" },
|
||||
REGEX_HARD_COL: { format: "$128.", label: "REGEX_HARD_COL", length: "128", type: "char" },
|
||||
REGEX_SOFT_COL: { format: "$128.", label: "REGEX_SOFT_COL", length: "128", type: "char" }
|
||||
}
|
||||
},
|
||||
sasparams: [
|
||||
{
|
||||
COLHEADERS: "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_HARDSELECT,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM,READONLY_COL,HIDDEN_COL,ROUND_COL,NUMFMT_COL",
|
||||
COLHEADERS: "_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_HARDSELECT,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM,READONLY_COL,HIDDEN_COL,ROUND_COL,NUMFMT_COL,REGEX_HARD_COL,REGEX_SOFT_COL",
|
||||
FILTER_TEXT: FILTER_TEXT,
|
||||
PKCNT: 1,
|
||||
PK: "PRIMARY_KEY_FIELD",
|
||||
@@ -372,10 +388,9 @@ let webouts = {
|
||||
cols: [
|
||||
{
|
||||
NAME: "DD_LONGDESC",
|
||||
VARNUM: 6,
|
||||
LABEL: "DD_LONGDESC",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -384,10 +399,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_OWNER",
|
||||
VARNUM: 7,
|
||||
LABEL: "DD_OWNER",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -396,10 +410,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_RESPONSIBLE",
|
||||
VARNUM: 8,
|
||||
LABEL: "DD_RESPONSIBLE",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -408,10 +421,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_SENSITIVITY",
|
||||
VARNUM: 9,
|
||||
LABEL: "DD_SENSITIVITY",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -420,10 +432,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_SHORTDESC",
|
||||
VARNUM: 5,
|
||||
LABEL: "DD_SHORTDESC",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -432,10 +443,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_SOURCE",
|
||||
VARNUM: 4,
|
||||
LABEL: "DD_SOURCE",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -444,10 +454,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DD_TYPE",
|
||||
VARNUM: 3,
|
||||
LABEL: "DD_TYPE",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -456,10 +465,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "TX_FROM",
|
||||
VARNUM: 1,
|
||||
LABEL: "TX_FROM",
|
||||
FMTNAME: "datetime",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -467,10 +475,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "TX_TO",
|
||||
VARNUM: 2,
|
||||
LABEL: "TX_TO",
|
||||
FMTNAME: "datetime",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -650,10 +657,9 @@ let webouts = {
|
||||
cols: [
|
||||
{
|
||||
NAME: "LAST_SEEN_DT",
|
||||
VARNUM: 2,
|
||||
LABEL: "LAST_SEEN_DT",
|
||||
FMTNAME: "date",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -661,10 +667,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "REGISTERED_DT",
|
||||
VARNUM: 3,
|
||||
LABEL: "REGISTERED_DT",
|
||||
FMTNAME: "date",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -672,10 +677,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "USER_ID",
|
||||
VARNUM: 1,
|
||||
LABEL: "USER_ID",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -777,10 +781,9 @@ let webouts = {
|
||||
cols: [
|
||||
{
|
||||
NAME: "AUDIT_LIBDS",
|
||||
VARNUM: 22,
|
||||
LABEL: "AUDIT_LIBDS",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -789,10 +792,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "BUSKEY",
|
||||
VARNUM: 7,
|
||||
LABEL: "BUSKEY",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -801,10 +803,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "CLOSE_VARS",
|
||||
VARNUM: 13,
|
||||
LABEL: "CLOSE_VARS",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -813,10 +814,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "DSN",
|
||||
VARNUM: 4,
|
||||
LABEL: "DSN",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -825,10 +825,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "LIBREF",
|
||||
VARNUM: 3,
|
||||
LABEL: "LIBREF",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -837,10 +836,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "LOADTYPE",
|
||||
VARNUM: 6,
|
||||
LABEL: "LOADTYPE",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -849,10 +847,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "NOTES",
|
||||
VARNUM: 20,
|
||||
LABEL: "NOTES",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -861,10 +858,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "NUM_OF_APPROVALS_REQUIRED",
|
||||
VARNUM: 5,
|
||||
LABEL: "NUM_OF_APPROVALS_REQUIRED",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "NUMERIC",
|
||||
DDTYPE: "N",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -873,10 +869,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "POST_APPROVE_HOOK",
|
||||
VARNUM: 17,
|
||||
LABEL: "POST_APPROVE_HOOK",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -885,10 +880,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "POST_EDIT_HOOK",
|
||||
VARNUM: 15,
|
||||
LABEL: "POST_EDIT_HOOK",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -897,10 +891,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "PRE_APPROVE_HOOK",
|
||||
VARNUM: 16,
|
||||
LABEL: "PRE_APPROVE_HOOK",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -909,10 +902,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "PRE_EDIT_HOOK",
|
||||
VARNUM: 14,
|
||||
LABEL: "PRE_EDIT_HOOK",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -921,10 +913,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "RK_UNDERLYING",
|
||||
VARNUM: 21,
|
||||
LABEL: "RK_UNDERLYING",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -933,10 +924,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SIGNOFF_COLS",
|
||||
VARNUM: 18,
|
||||
LABEL: "SIGNOFF_COLS",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -945,10 +935,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "SIGNOFF_HOOK",
|
||||
VARNUM: 19,
|
||||
LABEL: "SIGNOFF_HOOK",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -957,7 +946,6 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "TX_FROM",
|
||||
VARNUM: 1,
|
||||
LABEL: "TX_FROM",
|
||||
FMTNAME: "DATETIME",
|
||||
DDTYPE: "DATETIME",
|
||||
@@ -968,7 +956,6 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "TX_TO",
|
||||
VARNUM: 2,
|
||||
LABEL: "TX_TO",
|
||||
FMTNAME: "DATETIME",
|
||||
DDTYPE: "DATETIME",
|
||||
@@ -979,10 +966,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "VAR_BUSFROM",
|
||||
VARNUM: 10,
|
||||
LABEL: "VAR_BUSFROM",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -991,10 +977,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "VAR_BUSTO",
|
||||
VARNUM: 11,
|
||||
LABEL: "VAR_BUSTO",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -1003,10 +988,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "VAR_PROCESSED",
|
||||
VARNUM: 12,
|
||||
LABEL: "VAR_PROCESSED",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -1015,10 +999,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "VAR_TXFROM",
|
||||
VARNUM: 8,
|
||||
LABEL: "VAR_TXFROM",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -1027,10 +1010,9 @@ let webouts = {
|
||||
},
|
||||
{
|
||||
NAME: "VAR_TXTO",
|
||||
VARNUM: 9,
|
||||
LABEL: "VAR_TXTO",
|
||||
FMTNAME: "",
|
||||
DDTYPE: "CHARACTER",
|
||||
DDTYPE: "C",
|
||||
CLS_RULE: "READ",
|
||||
MEMLABEL: "",
|
||||
DESC: "",
|
||||
@@ -1519,7 +1501,7 @@ let webouts = {
|
||||
// MPE_X_TEST keeps matching the excel upload fixtures (which predate these
|
||||
// columns). The demo tables are built lazily (below) so we don't clone the large
|
||||
// MPE_X_TEST payload unless MPE_X_NEW is actually requested.
|
||||
const RULE_DEMO_COLS = ['SOME_HARDSELECT', 'READONLY_COL', 'HIDDEN_COL', 'ROUND_COL', 'NUMFMT_COL']
|
||||
const RULE_DEMO_COLS = ['SOME_HARDSELECT', 'READONLY_COL', 'HIDDEN_COL', 'ROUND_COL', 'NUMFMT_COL', 'REGEX_HARD_COL', 'REGEX_SOFT_COL']
|
||||
|
||||
function stripRuleCols(t) {
|
||||
const uc = new Set(RULE_DEMO_COLS)
|
||||
|
||||
@@ -31,37 +31,33 @@ let webouts = {
|
||||
{
|
||||
"NAME": "PRIMARY_KEY_FIELD",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 1,
|
||||
"LABEL": "PRIMARY_KEY_FIELD",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_BESTNUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 9,
|
||||
"LABEL": "SOME_BESTNUM",
|
||||
"FMTNAME": "BEST",
|
||||
"FORMAT": "BEST.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_CHAR",
|
||||
"LENGTH": 32767,
|
||||
"VARNUM": 2,
|
||||
"LABEL": "Some Character Column",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32767.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_DATE",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 5,
|
||||
"LABEL": "Some Date",
|
||||
"FMTNAME": "DATE",
|
||||
"FORMAT": "DATE9.",
|
||||
@@ -71,7 +67,6 @@ let webouts = {
|
||||
{
|
||||
"NAME": "SOME_DATETIME",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 6,
|
||||
"LABEL": "SOME_DATETIME",
|
||||
"FMTNAME": "DATETIME",
|
||||
"FORMAT": "DATETIME19.",
|
||||
@@ -81,37 +76,33 @@ let webouts = {
|
||||
{
|
||||
"NAME": "SOME_DROPDOWN",
|
||||
"LENGTH": 128,
|
||||
"VARNUM": 3,
|
||||
"LABEL": "SOME_DROPDOWN",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$128.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_NUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 4,
|
||||
"LABEL": "",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_SHORTNUM",
|
||||
"LENGTH": 4,
|
||||
"VARNUM": 8,
|
||||
"LABEL": "SOME_SHORTNUM",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "4.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "SOME_TIME",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 7,
|
||||
"LABEL": "SOME_TIME",
|
||||
"FMTNAME": "TIME",
|
||||
"FORMAT": "TIME8.",
|
||||
@@ -3093,117 +3084,105 @@ let webouts = {
|
||||
{
|
||||
"NAME": "DSN",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 3,
|
||||
"LABEL": "Dataset Name (32 chars)",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "IS_DIFF",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 9,
|
||||
"LABEL": "Did value change? (1/0/-1). Always -1 for appends and deletes.",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "IS_PK",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 8,
|
||||
"LABEL": "Is Primary Key Field? (1/0)",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "KEY_HASH",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 4,
|
||||
"LABEL": "MD5 Hash of primary key values (pipe seperated)",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "LIBREF",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 2,
|
||||
"LABEL": "Library Reference (8 chars)",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$8.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "LOAD_REF",
|
||||
"LENGTH": 36,
|
||||
"VARNUM": 1,
|
||||
"LABEL": "unique load reference",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$36.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "MOVE_TYPE",
|
||||
"LENGTH": 1,
|
||||
"VARNUM": 6,
|
||||
"LABEL": "Either (A)ppended, (D)eleted or (M)odified",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$1.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "NEWVAL_CHAR",
|
||||
"LENGTH": 32765,
|
||||
"VARNUM": 14,
|
||||
"LABEL": "New (character) value",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32765.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "NEWVAL_NUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 12,
|
||||
"LABEL": "New (numeric) value",
|
||||
"FMTNAME": "BEST",
|
||||
"FORMAT": "BEST32.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "OLDVAL_CHAR",
|
||||
"LENGTH": 32765,
|
||||
"VARNUM": 13,
|
||||
"LABEL": "Old (character) value",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32765.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "OLDVAL_NUM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 11,
|
||||
"LABEL": "Old (numeric) value",
|
||||
"FMTNAME": "BEST",
|
||||
"FORMAT": "BEST32.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "PROCESSED_DTTM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 7,
|
||||
"LABEL": "Processed at timestamp",
|
||||
"FMTNAME": "E8601DT",
|
||||
"FORMAT": "E8601DT26.6",
|
||||
@@ -3213,22 +3192,20 @@ let webouts = {
|
||||
{
|
||||
"NAME": "TGTVAR_NM",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 5,
|
||||
"LABEL": "Target variable name (32 chars)",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "TGTVAR_TYPE",
|
||||
"LENGTH": 1,
|
||||
"VARNUM": 10,
|
||||
"LABEL": "Either (C)haracter or (N)umeric",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$1.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
}
|
||||
],
|
||||
"dsmeta": [
|
||||
@@ -4426,12 +4403,12 @@ let webouts = {
|
||||
]
|
||||
, "cols":
|
||||
[
|
||||
{"NAME":"ALERT_DS" ,"LENGTH":32 ,"VARNUM":4 ,"LABEL":"ALERT_DS" ,"FMTNAME":"" ,"FORMAT":"$32." ,"TYPE":"C" ,"DDTYPE":"CHARACTER" }
|
||||
,{"NAME":"ALERT_EVENT" ,"LENGTH":20 ,"VARNUM":2 ,"LABEL":"ALERT_EVENT" ,"FMTNAME":"" ,"FORMAT":"$20." ,"TYPE":"C" ,"DDTYPE":"CHARACTER" }
|
||||
,{"NAME":"ALERT_LIB" ,"LENGTH":8 ,"VARNUM":3 ,"LABEL":"ALERT_LIB" ,"FMTNAME":"" ,"FORMAT":"$8." ,"TYPE":"C" ,"DDTYPE":"CHARACTER" }
|
||||
,{"NAME":"ALERT_USER" ,"LENGTH":100 ,"VARNUM":5 ,"LABEL":"ALERT_USER" ,"FMTNAME":"" ,"FORMAT":"$100." ,"TYPE":"C" ,"DDTYPE":"CHARACTER" }
|
||||
,{"NAME":"TX_FROM" ,"LENGTH":8 ,"VARNUM":1 ,"LABEL":"TX_FROM" ,"FMTNAME":"DATETIME" ,"FORMAT":"DATETIME19.3" ,"TYPE":"N" ,"DDTYPE":"DATETIME" }
|
||||
,{"NAME":"TX_TO" ,"LENGTH":8 ,"VARNUM":6 ,"LABEL":"TX_TO" ,"FMTNAME":"DATETIME" ,"FORMAT":"DATETIME19.3" ,"TYPE":"N" ,"DDTYPE":"DATETIME" }
|
||||
{"NAME":"ALERT_DS" ,"LENGTH":32 ,"LABEL":"ALERT_DS" ,"FMTNAME":"" ,"FORMAT":"$32." ,"TYPE":"C" ,"DDTYPE":"C" }
|
||||
,{"NAME":"ALERT_EVENT" ,"LENGTH":20 ,"LABEL":"ALERT_EVENT" ,"FMTNAME":"" ,"FORMAT":"$20." ,"TYPE":"C" ,"DDTYPE":"C" }
|
||||
,{"NAME":"ALERT_LIB" ,"LENGTH":8 ,"LABEL":"ALERT_LIB" ,"FMTNAME":"" ,"FORMAT":"$8." ,"TYPE":"C" ,"DDTYPE":"C" }
|
||||
,{"NAME":"ALERT_USER" ,"LENGTH":100 ,"LABEL":"ALERT_USER" ,"FMTNAME":"" ,"FORMAT":"$100." ,"TYPE":"C" ,"DDTYPE":"C" }
|
||||
,{"NAME":"TX_FROM" ,"LENGTH":8 ,"LABEL":"TX_FROM" ,"FMTNAME":"DATETIME" ,"FORMAT":"DATETIME19.3" ,"TYPE":"N" ,"DDTYPE":"DATETIME" }
|
||||
,{"NAME":"TX_TO" ,"LENGTH":8 ,"LABEL":"TX_TO" ,"FMTNAME":"DATETIME" ,"FORMAT":"DATETIME19.3" ,"TYPE":"N" ,"DDTYPE":"DATETIME" }
|
||||
]
|
||||
, "dsmeta":
|
||||
[
|
||||
@@ -4520,67 +4497,60 @@ let webouts = {
|
||||
{
|
||||
"NAME": "BASE_COL",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 4,
|
||||
"LABEL": "BASE_COL",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "BASE_DS",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 3,
|
||||
"LABEL": "BASE_DS",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "BASE_LIB",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 2,
|
||||
"LABEL": "BASE_LIB",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$8.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "RULE_ACTIVE",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 7,
|
||||
"LABEL": "RULE_ACTIVE",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "8.",
|
||||
"TYPE": "N",
|
||||
"DDTYPE": "NUMERIC"
|
||||
"DDTYPE": "N"
|
||||
},
|
||||
{
|
||||
"NAME": "RULE_TYPE",
|
||||
"LENGTH": 32,
|
||||
"VARNUM": 5,
|
||||
"LABEL": "RULE_TYPE",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$32.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "RULE_VALUE",
|
||||
"LENGTH": 128,
|
||||
"VARNUM": 6,
|
||||
"LABEL": "RULE_VALUE",
|
||||
"FMTNAME": "",
|
||||
"FORMAT": "$128.",
|
||||
"TYPE": "C",
|
||||
"DDTYPE": "CHARACTER"
|
||||
"DDTYPE": "C"
|
||||
},
|
||||
{
|
||||
"NAME": "TX_FROM",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 1,
|
||||
"LABEL": "TX_FROM",
|
||||
"FMTNAME": "DATETIME",
|
||||
"FORMAT": "DATETIME19.3",
|
||||
@@ -4590,7 +4560,6 @@ let webouts = {
|
||||
{
|
||||
"NAME": "TX_TO",
|
||||
"LENGTH": 8,
|
||||
"VARNUM": 8,
|
||||
"LABEL": "TX_TO",
|
||||
"FMTNAME": "DATETIME",
|
||||
"FORMAT": "DATETIME19.3",
|
||||
@@ -5395,10 +5364,10 @@ let webouts = {
|
||||
const v = JSON.parse(webouts.MPE_X_TEST)
|
||||
|
||||
v.cols.push(
|
||||
{ NAME: "READONLY_COL", LENGTH: 200, VARNUM: 11, LABEL: "READONLY_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
|
||||
{ NAME: "HIDDEN_COL", LENGTH: 200, VARNUM: 12, LABEL: "HIDDEN_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "CHARACTER" },
|
||||
{ NAME: "ROUND_COL", LENGTH: 8, VARNUM: 13, LABEL: "ROUND_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" },
|
||||
{ NAME: "NUMFMT_COL", LENGTH: 8, VARNUM: 14, LABEL: "NUMFMT_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "NUMERIC" }
|
||||
{ NAME: "READONLY_COL", LENGTH: 200, LABEL: "READONLY_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "C" },
|
||||
{ NAME: "HIDDEN_COL", LENGTH: 200, LABEL: "HIDDEN_COL", FMTNAME: "", FORMAT: "$200.", TYPE: "C", DDTYPE: "C" },
|
||||
{ NAME: "ROUND_COL", LENGTH: 8, LABEL: "ROUND_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "N" },
|
||||
{ NAME: "NUMFMT_COL", LENGTH: 8, LABEL: "NUMFMT_COL", FMTNAME: "", FORMAT: "BEST.", TYPE: "N", DDTYPE: "N" }
|
||||
)
|
||||
|
||||
v.viewdata = v.viewdata.map((row, i) => ({
|
||||
|
||||
Generated
+92
-101
@@ -6,8 +6,8 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.18.0",
|
||||
"@sasjs/core": "4.68.1"
|
||||
"@sasjs/cli": "4.18.4",
|
||||
"@sasjs/core": "4.68.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
@@ -200,65 +200,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.17.1.tgz",
|
||||
"integrity": "sha512-MxTvAoVnr+sBmKtB+CHxDiwvsqfzBqQ3BT/Q2Of6g7WWzJRBvpJcpf6C9R80Uq/cxvEie9JAM3EfExtFEYlcLg==",
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.17.2.tgz",
|
||||
"integrity": "sha512-VfQgTPgHxfnuGyXyPW1VaVwqY3KaUnllte212F2cxceXcUGfCVT6CpDn1GlYQgOyF7W1fsrmnrzc7j+xLxjlyA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^3.5.6",
|
||||
"axios": "1.16.0",
|
||||
"axios": "1.18.1",
|
||||
"axios-cookiejar-support": "5.0.5",
|
||||
"form-data": "4.0.6",
|
||||
"https": "1.0.0",
|
||||
"tough-cookie": "4.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter/node_modules/@sasjs/utils": {
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.8.tgz",
|
||||
"integrity": "sha512-rR2nCJG5AsuLj+nonpVO4PvA5OW8pUhDivY/25E4PCrQYLAmOaWQyIRne1gvSLL5ELzsOwRZWz6Zf6a02uNayA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/prompts": "2.0.13",
|
||||
"chalk": "4.1.1",
|
||||
"cli-table": "0.3.6",
|
||||
"consola": "2.15.0",
|
||||
"find": "0.3.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"jwt-decode": "3.1.2",
|
||||
"prompts": "2.4.1",
|
||||
"valid-url": "1.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter/node_modules/chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.18.0.tgz",
|
||||
"integrity": "sha512-QFsLS9XXF64x9w51kYkmx3ZUWCA9ycVDeLIaNJpHcaTu6jgPRAVHQ61ShXBKlYp/Vrze9pNapW43iaXt7o6VEA==",
|
||||
"version": "4.18.4",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.18.4.tgz",
|
||||
"integrity": "sha512-ZxAycth1GDqTsWPjDIx0uifdhmXsgC5LFx4SdujpThPK1bf5MNGLn4h2zqlBKlLTEWKn5UPXVXMDD8ktVn9wtg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "4.17.1",
|
||||
"@sasjs/core": "4.67.3",
|
||||
"@sasjs/adapter": "4.17.2",
|
||||
"@sasjs/core": "4.68.1",
|
||||
"@sasjs/lint": "2.4.3",
|
||||
"@sasjs/utils": "3.5.8",
|
||||
"adm-zip": "0.5.10",
|
||||
"@sasjs/utils": "3.5.9",
|
||||
"adm-zip": "0.6.0",
|
||||
"chalk": "4.1.2",
|
||||
"dotenv": "16.0.3",
|
||||
"find": "0.3.0",
|
||||
@@ -280,15 +245,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/core": {
|
||||
"version": "4.67.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.67.3.tgz",
|
||||
"integrity": "sha512-7s4UeVN4slF1URo1C5/UnNVv+TonUg+vBktFRiFhcbFYr3RnfXyClBPED6nQOF8AFd2xdXEss58hsCeIebr4lg==",
|
||||
"version": "4.68.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.1.tgz",
|
||||
"integrity": "sha512-BjoOzV7h7guCShW4JsFHvB1NNLCKHhPI5708OtEk29itQbgVCMRlEXKFRbnMiuNklktDQDi8dR4XZZ0aK7/4Wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/utils": {
|
||||
"version": "3.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.8.tgz",
|
||||
"integrity": "sha512-rR2nCJG5AsuLj+nonpVO4PvA5OW8pUhDivY/25E4PCrQYLAmOaWQyIRne1gvSLL5ELzsOwRZWz6Zf6a02uNayA==",
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.68.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.2.tgz",
|
||||
"integrity": "sha512-flbx36sNN7PvEIjX8H6UuGIKMQ8Ra3ZHEze9O66Wtk6cU+T0RV1i199LYXVumkja/VXSW334HlNpiSVR4o9qqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/lint": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/lint/-/lint-2.4.3.tgz",
|
||||
"integrity": "sha512-J0fYJmDfnhXDeMkZI8vHo8zjIsByIPcKHSQbiQEuRMXbNJXjqgTqoCpN0L6SlhyrrXUtMQvk/XErJ3OQGVEjrA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "3.5.2",
|
||||
"ignore": "5.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/lint/node_modules/@sasjs/utils": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.2.tgz",
|
||||
"integrity": "sha512-LBpBDx0T7G/eO15Gb+r3DR1LfBnoqagWT3HiHabojFziA4ej4ePORo8chrk0zHLIzrjI2ljAnDabyJEwC5KtIA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
@@ -304,7 +287,7 @@
|
||||
"valid-url": "1.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli/node_modules/@sasjs/utils/node_modules/chalk": {
|
||||
"node_modules/@sasjs/lint/node_modules/chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
@@ -320,28 +303,10 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/core": {
|
||||
"version": "4.68.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.1.tgz",
|
||||
"integrity": "sha512-BjoOzV7h7guCShW4JsFHvB1NNLCKHhPI5708OtEk29itQbgVCMRlEXKFRbnMiuNklktDQDi8dR4XZZ0aK7/4Wg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sasjs/lint": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/lint/-/lint-2.4.3.tgz",
|
||||
"integrity": "sha512-J0fYJmDfnhXDeMkZI8vHo8zjIsByIPcKHSQbiQEuRMXbNJXjqgTqoCpN0L6SlhyrrXUtMQvk/XErJ3OQGVEjrA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "3.5.2",
|
||||
"ignore": "5.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.2.tgz",
|
||||
"integrity": "sha512-LBpBDx0T7G/eO15Gb+r3DR1LfBnoqagWT3HiHabojFziA4ej4ePORo8chrk0zHLIzrjI2ljAnDabyJEwC5KtIA==",
|
||||
"hasInstallScript": true,
|
||||
"version": "3.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.9.tgz",
|
||||
"integrity": "sha512-Qg4N8LoR51vN8WyYUap2OyvJi+6woJk7ph5ylbFVomO9CM+7KIvkMJLFAtwmKSccWPn+FSy2Lhi39pQ/HA1dug==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
@@ -418,12 +383,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
|
||||
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
|
||||
"integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
"node": ">=14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
@@ -466,13 +431,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
|
||||
"integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
@@ -890,9 +856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
@@ -1251,16 +1217,28 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent/node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
@@ -1476,10 +1454,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom/node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
@@ -1657,9 +1648,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -2330,9 +2321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.18.0",
|
||||
"@sasjs/core": "4.68.1"
|
||||
"@sasjs/cli": "4.18.4",
|
||||
"@sasjs/core": "4.68.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
@file
|
||||
@brief migration script to move to v7.11 of Data Controller
|
||||
|
||||
OPTIONAL CHANGE - upload additional validation rules (HARDREGEX, SOFTREGEX)
|
||||
into the RULE_TYPE dropdown (data only)
|
||||
|
||||
**/
|
||||
|
||||
%let dclib=YOURDCLIB;
|
||||
|
||||
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
|
||||
|
||||
|
||||
/* add new validation rules */
|
||||
proc sql noprint;
|
||||
select max(selectbox_rk) into: maxrk
|
||||
from &dclib..mpe_selectbox;
|
||||
insert into &dclib..mpe_selectbox set
|
||||
selectbox_rk=&maxrk+1
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&dclib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value='HARDREGEX'
|
||||
,selectbox_order=13
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &dclib..mpe_selectbox set
|
||||
selectbox_rk=&maxrk+2
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&dclib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value='SOFTREGEX'
|
||||
,selectbox_order=14
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
quit;
|
||||
|
||||
|
||||
/*
|
||||
Update the MPE_SECURITY LIBREF validation from HARDSELECT on MPE_TABLES
|
||||
(EDITABLE libraries only) to the SOFTSELECT_HOOK, which lists ALL libraries
|
||||
(services/validations/libraries_all)
|
||||
*/
|
||||
proc sql noprint;
|
||||
/* close out the current record */
|
||||
update &dclib..mpe_validations set
|
||||
tx_to=%sysfunc(datetime())
|
||||
where base_lib="&dclib"
|
||||
and base_ds="MPE_SECURITY"
|
||||
and base_col="LIBREF"
|
||||
and rule_type='HARDSELECT'
|
||||
and tx_to='31DEC5999:23:59:59'dt;
|
||||
/* insert the new version */
|
||||
insert into &dclib..mpe_validations set
|
||||
tx_from=%sysfunc(datetime())
|
||||
,base_lib="&dclib"
|
||||
,base_ds="MPE_SECURITY"
|
||||
,base_col="LIBREF"
|
||||
,rule_type='SOFTSELECT_HOOK'
|
||||
,rule_value="services/validations/libraries_all"
|
||||
,rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
quit;
|
||||
@@ -916,6 +916,24 @@ insert into &lib..mpe_selectbox set
|
||||
,selectbox_value="SOFTSELECT_HOOK"
|
||||
,selectbox_order=12
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="HARDREGEX"
|
||||
,selectbox_order=13
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
,select_lib="&lib"
|
||||
,select_ds="MPE_VALIDATIONS"
|
||||
,base_column="RULE_TYPE"
|
||||
,selectbox_value="SOFTREGEX"
|
||||
,selectbox_order=14
|
||||
,ver_to_dttm='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..mpe_selectbox set
|
||||
selectbox_rk=%mf_increment(rk)
|
||||
,ver_from_dttm=0
|
||||
@@ -1974,6 +1992,26 @@ insert into &lib..MPE_VALIDATIONS set
|
||||
,rule_value="&lib..MPE_X_TEST.SOME_BESTNUM"
|
||||
,rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
/* test hard regex - SOME_CHAR must contain "the" or "data" */
|
||||
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;
|
||||
/* test soft regex - SOME_CHAR should contain the letter "t" */
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0
|
||||
,base_lib="&lib"
|
||||
,base_ds="MPE_X_TEST"
|
||||
,base_col="SOME_CHAR"
|
||||
,rule_type='SOFTREGEX'
|
||||
,rule_value='/t/'
|
||||
,rule_active=1
|
||||
,tx_to='31DEC5999:23:59:59'dt;
|
||||
insert into &lib..MPE_VALIDATIONS set
|
||||
tx_from=0
|
||||
,base_lib="&lib"
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
@brief Testing mpe_refreshtables macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mpe_refreshlibs.sas
|
||||
@li mpe_refreshtables.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
@li mf_mkdir.sas
|
||||
@li mp_assert.sas
|
||||
@li mp_assertscope.sas
|
||||
|
||||
@@ -14,14 +17,32 @@
|
||||
|
||||
**/
|
||||
|
||||
/* create a new library with a demo format catalog, so it always exists */
|
||||
%let reftstloc=%sysfunc(pathname(&dc_libref))/reftst%mf_getuniquefileref();
|
||||
%mf_mkdir(&reftstloc)
|
||||
libname reftst "&reftstloc";
|
||||
|
||||
data work.reftstfmts;
|
||||
length fmtname $32;
|
||||
fmtname='REFFMT';
|
||||
do start=1 to 10;
|
||||
label=cats('Format ',start);
|
||||
output;
|
||||
end;
|
||||
run;
|
||||
proc format cntlin=work.reftstfmts library=reftst.dcfmts;
|
||||
run;
|
||||
|
||||
/* register the library in mpe_datacatalog_libs */
|
||||
%mpe_refreshlibs(lib=REFTST)
|
||||
|
||||
%mp_assertscope(SNAPSHOT)
|
||||
%mpe_refreshtables(FMTONLY)
|
||||
%mpe_refreshtables(REFTST)
|
||||
%mp_assertscope(COMPARE,
|
||||
desc=Checking macro variables against previous snapshot
|
||||
)
|
||||
|
||||
/* make sure that the process picks up a library that contains only a single
|
||||
catalog */
|
||||
/* make sure that the process picks up a library that contains a catalog */
|
||||
proc sql;
|
||||
create table work.libinfo as
|
||||
select a.engine,
|
||||
@@ -37,7 +58,7 @@ create table work.libinfo as
|
||||
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
|
||||
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on a.libref=b.libref
|
||||
where a.libref="FMTONLY";
|
||||
where a.libref="REFTST";
|
||||
|
||||
%let test1=0;
|
||||
data _null_;
|
||||
@@ -46,5 +67,5 @@ data _null_;
|
||||
run;
|
||||
%mp_assert(
|
||||
iftrue=(&test1>0),
|
||||
desc=Checking fmtonly.dcfmts was picked up
|
||||
desc=Checking reftst.dcfmts was picked up
|
||||
)
|
||||
@@ -105,7 +105,7 @@ run;
|
||||
desc=Checking leading blanks were applied
|
||||
)
|
||||
%mp_assert(
|
||||
iftrue=(&leadcheck=1),
|
||||
iftrue=(&speshcheck=1),
|
||||
desc=Checking special characters were applied
|
||||
)
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@
|
||||
<h5> cols </h5>
|
||||
Contains column level attributes.
|
||||
@li NAME - column name
|
||||
@li VARNUM - var position. https://core.sasjs.io/mp__getcols_8sas.html
|
||||
@li LABEL - var label. https://core.sasjs.io/mp__getcols_8sas.html
|
||||
@li FMTNAME - derived format. https://core.sasjs.io/mp__getcols_8sas.html
|
||||
@li DDTYPE - derived dropdown. https://core.sasjs.io/mp__getcols_8sas.html
|
||||
@li DDTYPE - derived dropdown. C=CHARACTER, N=NUMERIC, else DATE / TIME /
|
||||
DATETIME. https://core.sasjs.io/mp__getcols_8sas.html
|
||||
@li CLS_RULE - values include:
|
||||
- EDIT - the column is editable
|
||||
- READ - the column should be readonly
|
||||
@@ -488,14 +488,18 @@ select upcase(loadtype)
|
||||
/* extract col info */
|
||||
%mp_getcols(&libds, outds=cols1)
|
||||
|
||||
/* join with cls rules */
|
||||
/* join with cls rules. Trim COLS payload in the SQL below - DDTYPE
|
||||
C=CHARACTER N=NUMERIC (dates kept in full), VARNUM dropped (client
|
||||
relies on column order, not position) */
|
||||
proc sql;
|
||||
create table work.cols as
|
||||
select a.NAME
|
||||
,a.VARNUM
|
||||
,c.desc as LABEL
|
||||
,coalesce(c.desc,a.NAME) as LABEL
|
||||
,a.FMTNAME
|
||||
,a.DDTYPE
|
||||
,case a.DDTYPE
|
||||
when 'CHARACTER' then 'C'
|
||||
when 'NUMERIC' then 'N'
|
||||
else a.DDTYPE end as DDTYPE
|
||||
,case b.cls_hide
|
||||
when 1 then 'HIDE'
|
||||
when 0 then 'EDIT'
|
||||
@@ -503,7 +507,7 @@ create table work.cols as
|
||||
,c.memlabel
|
||||
,c.longdesc
|
||||
,d.colType
|
||||
from work.cols1 a
|
||||
from work.cols1(drop=varnum) a
|
||||
left join work.cls_rules b
|
||||
on a.NAME=b.CLS_VARIABLE_NM
|
||||
left join work.spec c
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
@file
|
||||
@brief testing stagedata RLS / REPLACE loadtype incompatibility (#211)
|
||||
@details Backend validations should abort when:
|
||||
1. loading row level EDIT security against a table with LOADTYPE=REPLACE
|
||||
2. applying LOADTYPE=REPLACE to a table with row level EDIT security
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mx_testservice.sas
|
||||
@li mp_assert.sas
|
||||
|
||||
|
||||
**/
|
||||
|
||||
%let _program=&appLoc/services/editors/stagedata;
|
||||
|
||||
/**
|
||||
* Prep - register a DCTEST table with REPLACE loadtype (direct insert,
|
||||
* bypassing stagedata) and create the physical table
|
||||
*/
|
||||
data dctest.dc_replace;
|
||||
x=1;
|
||||
run;
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_tables where libref="DCTEST" and dsn='DC_REPLACE';
|
||||
insert into &dc_libref..mpe_tables
|
||||
set tx_from=0
|
||||
,tx_to='31DEC5999:23:59:59'dt
|
||||
,libref="DCTEST"
|
||||
,dsn='DC_REPLACE'
|
||||
,buskey='X'
|
||||
,loadtype='REPLACE'
|
||||
,num_of_approvals_required=1;
|
||||
quit;
|
||||
|
||||
/**
|
||||
* Test 1 - submitting an RLS EDIT rule against a REPLACE table should abort
|
||||
*/
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='RLS EDIT rule on REPLACE table should abort';
|
||||
libds="&dc_libref..MPE_ROW_LEVEL_SECURITY";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
length RLS_SCOPE $8 RLS_GROUP $128 RLS_LIBREF $8 RLS_TABLE $32
|
||||
RLS_GROUP_LOGIC $3 RLS_SUBGROUP_LOGIC $3 RLS_VARIABLE_NM $32
|
||||
RLS_OPERATOR_NM $12 RLS_RAW_VALUE $4000
|
||||
_____DELETE__THIS__RECORD_____ $3;
|
||||
RLS_SCOPE='EDIT';
|
||||
RLS_GROUP='SASAdministrators';
|
||||
RLS_LIBREF='DCTEST';
|
||||
RLS_TABLE='DC_REPLACE';
|
||||
RLS_GROUP_LOGIC='AND';
|
||||
RLS_SUBGROUP_LOGIC='OR';
|
||||
RLS_SUBGROUP_ID=0;
|
||||
RLS_VARIABLE_NM='X';
|
||||
RLS_OPERATOR_NM='NE';
|
||||
RLS_RAW_VALUE='1';
|
||||
RLS_ACTIVE=1;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
output;
|
||||
run;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web1,
|
||||
outref=wb1,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
/* search terms may span multiple lines, so track matches across records */
|
||||
%let abort1=0;
|
||||
data _null_;
|
||||
retain foundabort foundmsg 0;
|
||||
infile wb1;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if index(_infile_,'sasjsAbort') then foundabort=1;
|
||||
if index(_infile_,'REPLACE loadtype') then foundmsg=1;
|
||||
if foundabort=1 and foundmsg=1 then call symputx('abort1',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort1=1),
|
||||
desc=Checking RLS EDIT rule is rejected for REPLACE loadtype table (#211),
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 2 - applying REPLACE loadtype to a table with RLS EDIT security
|
||||
* should abort. First insert an active EDIT rule directly.
|
||||
*/
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_row_level_security
|
||||
where rls_libref="DCTEST" and rls_table='WIDEBOY';
|
||||
select coalesce(max(rls_rk),0)+1 into: rlsrk
|
||||
from &dc_libref..mpe_row_level_security;
|
||||
insert into &dc_libref..mpe_row_level_security
|
||||
set tx_from=0
|
||||
,tx_to='31DEC5999:23:59:59'dt
|
||||
,rls_rk=&rlsrk
|
||||
,rls_scope='EDIT'
|
||||
,rls_group='SASAdministrators'
|
||||
,rls_libref='DCTEST'
|
||||
,rls_table='WIDEBOY'
|
||||
,rls_group_logic='AND'
|
||||
,rls_subgroup_logic='OR'
|
||||
,rls_subgroup_id=0
|
||||
,rls_variable_nm='ROW_ID'
|
||||
,rls_operator_nm='NE'
|
||||
,rls_raw_value='1'
|
||||
,rls_active=1;
|
||||
quit;
|
||||
|
||||
/* now stage a REPLACE loadtype for DCTEST.WIDEBOY */
|
||||
data work.sascontroltable;
|
||||
action='LOAD';
|
||||
message='REPLACE loadtype on RLS-secured table should abort';
|
||||
libds="&dc_libref..MPE_TABLES";
|
||||
output;
|
||||
stop;
|
||||
run;
|
||||
|
||||
data work.jsdata;
|
||||
set &dc_libref..mpe_tables(where=(libref='DCTEST' and dsn='WIDEBOY'));
|
||||
loadtype='REPLACE';
|
||||
length _____DELETE__THIS__RECORD_____ $3;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web2,
|
||||
outref=wb2,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let abort2=0;
|
||||
data _null_;
|
||||
retain foundabort foundmsg 0;
|
||||
infile wb2;
|
||||
input;
|
||||
putlog _infile_;
|
||||
if index(_infile_,'sasjsAbort') then foundabort=1;
|
||||
if index(_infile_,'EDIT security') then foundmsg=1;
|
||||
if foundabort=1 and foundmsg=1 then call symputx('abort2',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&abort2=1),
|
||||
desc=Checking REPLACE loadtype is rejected for RLS EDIT-secured table (#211),
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/**
|
||||
* Test 3 - sanity check: REPLACE loadtype on an unsecured table is fine
|
||||
*/
|
||||
data work.jsdata;
|
||||
set &dc_libref..mpe_tables(where=(libref='DCTEST' and dsn='WIDEBOY'));
|
||||
loadtype='UPDATE';
|
||||
length _____DELETE__THIS__RECORD_____ $3;
|
||||
_____DELETE__THIS__RECORD_____='No';
|
||||
run;
|
||||
|
||||
/* remove the RLS rule first (cleanup of test 2) */
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_row_level_security
|
||||
where rls_libref="DCTEST" and rls_table='WIDEBOY';
|
||||
quit;
|
||||
|
||||
%mx_testservice(&_program,
|
||||
viyacontext=&defaultcontext,
|
||||
inputdatasets=work.sascontroltable work.jsdata,
|
||||
outlib=web3,
|
||||
mdebug=&sasjs_mdebug
|
||||
)
|
||||
|
||||
%let status3=0;
|
||||
data _null_;
|
||||
set web3.sasparams;
|
||||
putlog (_all_)(=);
|
||||
if status='SUCCESS' then call symputx('status3',1);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&status3=1),
|
||||
desc=Checking non-REPLACE loadtype still stages successfully after cleanup,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* final cleanup - remove the REPLACE table registration */
|
||||
proc sql noprint;
|
||||
delete from &dc_libref..mpe_tables where libref="DCTEST" and dsn='DC_REPLACE';
|
||||
quit;
|
||||
@@ -13,8 +13,12 @@
|
||||
This validation checks the incoming row_level_security settings to ensure
|
||||
each individual filter is
|
||||
|
||||
It also checks that EDIT scope security is not being applied to tables
|
||||
with a REPLACE loadtype (these are incompatible - see issue #211).
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li dc_assignlib.sas
|
||||
@li mp_abort.sas
|
||||
@li mp_filtercheck.sas
|
||||
|
||||
<h4> Related Macros </h4>
|
||||
@@ -23,6 +27,33 @@
|
||||
**/
|
||||
|
||||
|
||||
/* REPLACE loadtype is incompatible with row level EDIT security (#211) */
|
||||
proc sql noprint;
|
||||
create table work.badloadtypes as
|
||||
select distinct cats(upcase(a.rls_libref),'.',upcase(a.rls_table)) as libds
|
||||
from work.staging_ds(where=(rls_active=1
|
||||
and upcase(rls_scope) in ('EDIT','ALL')
|
||||
and upcase(_____DELETE__THIS__RECORD_____) ne 'YES')) a
|
||||
inner join &dc_libref..mpe_tables(where=(&dc_dttmtfmt. lt tx_to)) b
|
||||
on upcase(a.rls_libref)=upcase(b.libref)
|
||||
and upcase(a.rls_table)=upcase(b.dsn)
|
||||
where upcase(b.loadtype)='REPLACE';
|
||||
|
||||
%let badloadtypes=0;
|
||||
%let badloadtype=;
|
||||
data _null_;
|
||||
set work.badloadtypes;
|
||||
call symputx('badloadtypes',_n_);
|
||||
call symputx('badloadtype',libds);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&badloadtypes ne 0)
|
||||
,mac=mpe_row_level_security_postedit
|
||||
,msg=%str(Row level EDIT security cannot be applied to table(s) with a%trim(
|
||||
) REPLACE loadtype, eg: &badloadtype. Remove the security rule(s) or%trim(
|
||||
) change the loadtype.)
|
||||
)
|
||||
|
||||
/* ignore scope and group for validation */
|
||||
proc sql;
|
||||
create table work.batches as
|
||||
|
||||
@@ -69,6 +69,33 @@ run;
|
||||
,msg=%superq(errmsg)
|
||||
)
|
||||
|
||||
/* REPLACE loadtype is incompatible with row level EDIT security (#211) */
|
||||
proc sql noprint;
|
||||
create table work.badloadtypes as
|
||||
select distinct cats(upcase(a.libref),'.',upcase(a.dsn)) as libds
|
||||
from work.staging_ds(where=(loadtype='REPLACE'
|
||||
and upcase(_____DELETE__THIS__RECORD_____) ne 'YES')) a
|
||||
inner join &dc_libref..mpe_row_level_security(where=(
|
||||
rls_active=1 and &dc_dttmtfmt. lt tx_to
|
||||
and upcase(rls_scope) in ('EDIT','ALL'))) b
|
||||
on upcase(a.libref)=upcase(b.rls_libref)
|
||||
and upcase(a.dsn)=upcase(b.rls_table);
|
||||
|
||||
%let badloadtypes=0;
|
||||
%let badloadtype=;
|
||||
data _null_;
|
||||
set work.badloadtypes;
|
||||
call symputx('badloadtypes',_n_);
|
||||
call symputx('badloadtype',libds);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&badloadtypes ne 0)
|
||||
,mac=mpe_tables_postedit
|
||||
,msg=%str(REPLACE loadtype cannot be applied to table(s) with row level%trim(
|
||||
) EDIT security, eg: &badloadtype. Remove the security rule(s) or change%trim(
|
||||
) the loadtype.)
|
||||
)
|
||||
|
||||
/* get distinct list of audit libs */
|
||||
proc sql;
|
||||
create table work.liblist as
|
||||
|
||||
@@ -70,3 +70,38 @@ run;
|
||||
,msg=%str(The following vars have duplicate HOOKS: &src_list2)
|
||||
)
|
||||
|
||||
|
||||
/** check that any supplied HARDREGEX / SOFTREGEX rules contain a valid
|
||||
regex pattern. The frontend evaluates these as JavaScript regexes
|
||||
(invalid ones are silently ignored there), so prxparse is used here as
|
||||
a best-effort syntax check to catch typos at config time.
|
||||
An empty pattern is treated as valid (matches everything in JS). */
|
||||
data work.regex_check;
|
||||
length source $74;
|
||||
set work.staging_ds(
|
||||
where=(rule_type in ('HARDREGEX','SOFTREGEX')
|
||||
and upcase(_____DELETE__THIS__RECORD_____) ne "YES")
|
||||
);
|
||||
keep source rule_value;
|
||||
if rule_value ne ' ' then do;
|
||||
rx=prxparse(strip(rule_value));
|
||||
if missing(rx) then do;
|
||||
source=catx('.',base_lib,base_ds,base_col);
|
||||
output;
|
||||
end;
|
||||
else call prxfree(rx);
|
||||
end;
|
||||
run;
|
||||
|
||||
%global src_list3;
|
||||
%let src_list3='';
|
||||
proc sql noprint;
|
||||
select distinct quote(cats(source)) into: src_list3 separated by ','
|
||||
from work.regex_check;
|
||||
quit;
|
||||
|
||||
%mp_abort(iftrue= (%mf_nobs(work.regex_check)>0)
|
||||
,mac=mpe_validations_postedit
|
||||
,msg=%str(The following vars have invalid REGEX patterns: &src_list3)
|
||||
)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ data cols(keep=name type length varnum format label);
|
||||
else if formatl=0 then format=cats(format2,'.');
|
||||
else format=cats(format2,formatl,'.');
|
||||
type='C';
|
||||
ddtype='CHARACTER';
|
||||
ddtype='C';
|
||||
end;
|
||||
else do;
|
||||
if format2='' then format=cats(length,'.');
|
||||
@@ -44,7 +44,7 @@ data cols(keep=name type length varnum format label);
|
||||
if format=:'DATETIME' then ddtype='DATETIME';
|
||||
else if format=:'DATE' then ddtype='DATE';
|
||||
else if format=:'TIME' then ddtype='TIME';
|
||||
else ddtype='NUMERIC';
|
||||
else ddtype='N';
|
||||
end;
|
||||
if label='' then label=name;
|
||||
run;
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
<h4> Service Outputs </h4>
|
||||
|
||||
<h5> cols </h5>
|
||||
@li DDTYPE
|
||||
@li DDTYPE - C=CHARACTER, N=NUMERIC, else DATE / TIME / DATETIME
|
||||
@li FORMAT
|
||||
@li LABEL
|
||||
@li LENGTH
|
||||
@li NAME
|
||||
@li TYPE
|
||||
@li VARNUM
|
||||
|
||||
<h5> sasparams </h5>
|
||||
@li FILTER_TEXT
|
||||
@@ -361,11 +360,23 @@ run;
|
||||
)
|
||||
|
||||
%mp_getcols(&libds, outds=cols1)
|
||||
|
||||
/* trim COLS payload in the SQL below - DDTYPE C=CHARACTER N=NUMERIC
|
||||
(dates kept in full), VARNUM dropped (client relies on column order,
|
||||
not position) */
|
||||
proc sql;
|
||||
create table cols(drop=srclabel) as
|
||||
select a.*
|
||||
,coalesce(b.dd_shortdesc,a.srclabel) as label
|
||||
from cols1(rename=(label=srclabel)) a
|
||||
create table cols as
|
||||
select a.name
|
||||
,a.type
|
||||
,a.length
|
||||
,a.format
|
||||
,a.fmtname
|
||||
,case a.ddtype
|
||||
when 'CHARACTER' then 'C'
|
||||
when 'NUMERIC' then 'N'
|
||||
else a.ddtype end as ddtype
|
||||
,coalesce(b.dd_shortdesc,a.label,a.name) as label
|
||||
from cols1 a
|
||||
left join &mpelib..mpe_datadictionary
|
||||
(where=(&dc_dttmtfmt. < tx_to
|
||||
and dd_source ? %upcase("&orig_libds")
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
|
||||
**/
|
||||
|
||||
%global path ADMIN ;
|
||||
%global path ADMIN OUTFMT;
|
||||
|
||||
/* response format - HTML (default) or JSON */
|
||||
%let OUTFMT=HTML;
|
||||
|
||||
%webout(FETCH)
|
||||
|
||||
@@ -39,6 +42,19 @@ data _null_;
|
||||
call symputx('ADMIN',ADMIN);
|
||||
run;
|
||||
|
||||
/* check if a JSON response was requested (OUTFMT column in fromjs) */
|
||||
%if &exist=1 %then %do;
|
||||
data _null_;
|
||||
dsid=open('work.fromjs');
|
||||
if varnum(dsid,'OUTFMT')>0 then do;
|
||||
set work.fromjs;
|
||||
call symputx('OUTFMT',upcase(outfmt));
|
||||
end;
|
||||
rc=close(dsid);
|
||||
stop;
|
||||
run;
|
||||
%end;
|
||||
|
||||
%mp_abort(iftrue= (&syscc ne 0)
|
||||
,mac=&_program
|
||||
,msg=%str(Issue on makedata entry)
|
||||
@@ -140,6 +156,20 @@ run;
|
||||
)
|
||||
|
||||
|
||||
%if &OUTFMT=JSON %then %do;
|
||||
data work.makedata_response;
|
||||
length STATUS $10 MESSAGE $256 DCPATH $256 LIBREF $8 ADMIN $64;
|
||||
STATUS='SUCCESS';
|
||||
MESSAGE="Data Controller Config created by &sysuserid";
|
||||
DCPATH="&dcpath";
|
||||
LIBREF="&DC_LIBREF";
|
||||
ADMIN="&admin";
|
||||
run;
|
||||
%webout(OPEN)
|
||||
%webout(OBJ, makedata_response)
|
||||
%webout(CLOSE)
|
||||
%end;
|
||||
%else %do;
|
||||
data _null_;
|
||||
file _webout;
|
||||
put '<h3>Data Controller Config</h3>';
|
||||
@@ -159,6 +189,7 @@ data _null_;
|
||||
put "<li><a target='_blank' href='/AppStream/DataController'>Launch!</a>";
|
||||
put "</li></ol>";
|
||||
run;
|
||||
%end;
|
||||
|
||||
/* We ran successfully, now remove configurator and makedata STPs */
|
||||
%ms_deletefile(&root/services/admin/configurator.sas)
|
||||
|
||||
Reference in New Issue
Block a user