Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b50efc01e2 | ||
|
|
de23966721 | ||
|
|
9121bf900d | ||
|
|
56175353d6 | ||
|
|
26b4c55ecd | ||
|
|
5be23393d6 | ||
|
|
bf3ef8bab1 | ||
|
|
729437308a | ||
|
|
244829edf9 | ||
|
|
b51c831b53 | ||
|
|
8e0b28ed93 | ||
|
|
5b6575ae8e | ||
|
|
9dcedfffb0 | ||
|
|
34846e5a4c | ||
|
|
26304f3c73 | ||
|
|
14b745e2e7 | ||
|
|
bdddfac52a | ||
|
|
2cc3a13340 | ||
|
|
ebd77c9d76 | ||
|
|
73fccc70a5 | ||
|
|
8a10d127c1 | ||
|
|
2c42ecae5f | ||
|
|
2fee73cb9a | ||
|
|
eae81f1f6d | ||
|
|
efaa05549b | ||
|
|
8b2603f804 | ||
|
|
b58387bee5 | ||
|
|
53aa81c788 | ||
|
|
d2c8e5e6bc | ||
|
|
50833b4fd8 | ||
|
|
1f4e159fe0 | ||
|
|
ae55a82b19 | ||
|
|
bf3e48654c | ||
|
|
9c4c1e9d99 | ||
|
|
b543b5ab96 | ||
|
|
19743c7dd5 | ||
|
|
39ab4be7f7 | ||
|
|
51761ca3bd | ||
|
|
72e01e4c02 | ||
|
|
1b7a29b2d6 | ||
|
|
2120a3892a | ||
|
|
90ddd63370 | ||
|
|
ed1c90c057 | ||
|
|
4a1a420e39 | ||
|
|
54ddedd1af | ||
|
|
08070f3586 | ||
|
|
eda6c62f03 | ||
|
|
4a30ce77db | ||
|
|
85d7f4c61c | ||
|
|
a95d63d12e | ||
|
|
8434c781d6 | ||
|
|
6537e3aee1 | ||
|
|
f17ab261ce | ||
|
|
67eabbe55f | ||
|
|
0ed1caa651 | ||
|
|
e566abab23 | ||
|
|
148c1b66a7 | ||
|
|
dc0db8e8f2 | ||
|
|
dfdd103f18 | ||
|
|
f2ba44495f | ||
|
|
bcf9b57595 | ||
|
|
73b4f0ec3c | ||
|
|
9b2dc1ce60 | ||
|
|
d89d0dd7c0 | ||
|
|
00c6fa9fbd | ||
|
|
f4e9518159 | ||
|
|
05c7746474 | ||
|
|
0b178d66be | ||
|
|
bc9352f371 | ||
|
|
d2509decd0 |
@@ -0,0 +1,36 @@
|
||||
# Domain Docs
|
||||
|
||||
How to consume this repo's domain documentation when exploring the codebase.
|
||||
|
||||
## Before exploring, read these
|
||||
|
||||
- **`CONTEXT.md`** at the repo root - the domain glossary and orientation.
|
||||
- **`.agents/adr/`** - read the Architecture Decision Records that touch the area you're about to work in.
|
||||
|
||||
If any of these files don't exist, proceed silently. Don't flag their absence or suggest creating them upfront; they are created lazily when terms or decisions actually get resolved.
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── .agents/
|
||||
│ ├── adr/
|
||||
│ │ ├── 0001-<decision-slug>.md
|
||||
│ │ └── 0002-<decision-slug>.md
|
||||
│ ├── docs/
|
||||
│ └── skills/
|
||||
└── src/
|
||||
```
|
||||
|
||||
## Use the glossary's vocabulary
|
||||
|
||||
When your output names a domain concept (an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary avoids.
|
||||
|
||||
If the concept you need isn't in the glossary yet, that's a signal - either you're inventing language the project doesn't use (reconsider), or there's a real gap (note it).
|
||||
|
||||
## Flag ADR conflicts
|
||||
|
||||
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||
|
||||
> _Contradicts ADR-0001 (...) - but worth reopening because..._
|
||||
@@ -0,0 +1,19 @@
|
||||
# Issue tracker: Local Markdown
|
||||
|
||||
Issues and specs (you may know a spec as a PRD) for this repo live as markdown files in `.scratch/`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- One feature per directory: `.scratch/<feature-slug>/`
|
||||
- The spec is `.scratch/<feature-slug>/spec.md`
|
||||
- Implementation issues are one file per ticket at `.scratch/<feature-slug>/issues/<NN>-<slug>.md`, numbered from `01` - never a single combined tickets file
|
||||
- Triage state is recorded as a `Status:` line near the top of each issue file
|
||||
- Comments and conversation history append to the bottom of the file under a `## Comments` heading
|
||||
|
||||
## When a skill says "publish to the issue tracker"
|
||||
|
||||
Create a new file under `.scratch/<feature-slug>/` (creating the directory if needed).
|
||||
|
||||
## When a skill says "fetch the relevant ticket"
|
||||
|
||||
Read the file at the referenced path. The user will normally pass the path or the issue number directly.
|
||||
@@ -0,0 +1,63 @@
|
||||
# LinkedIn Post Writing Guide
|
||||
|
||||
Guidance for drafting LinkedIn posts promoting Data Controller (datacontroller.io).
|
||||
|
||||
## Audience
|
||||
|
||||
Our readers are typically:
|
||||
|
||||
- **Technical managers and platform owners** in regulated industries (Banking, Insurance, Pharma) running enterprise SAS® estates.
|
||||
- **Data governance / data quality leads** dealing with End User Computing (EUC) risk and audit requirements.
|
||||
- **SAS developers and administrators** who maintain data pipelines and care about automation, security and auditability.
|
||||
- Occasionally **C-level / procurement** - keep business value (OpEx, risk reduction, compliance) visible alongside technical detail.
|
||||
|
||||
Implications:
|
||||
|
||||
- Assume competence - no need to explain what ETL, SAS, or a data warehouse is.
|
||||
- Speak to their pains: regulatory reporting deadlines, audit trails, hand-cranked ETL, EUC sprawl, OpEx, blame-games between business and IT.
|
||||
- Professional but direct tone. Avoid hype, buzzword stuffing, and exclamation-mark overload.
|
||||
|
||||
## Structure
|
||||
|
||||
1. **Hook** - first 1-2 lines must stand alone; LinkedIn truncates the rest behind "…see more". State the pain or the claim, not the product.
|
||||
2. **Problem** - short paragraphs describing the pain in the reader's context.
|
||||
3. **Belief / shift** - what we believe the world should look like.
|
||||
4. **Solution** - introduce Data Controller, one or two sentences on what it does (zero-code data capture, review/approve workflow, audit trail, loads into SAS / any database).
|
||||
5. **Outcome** - the concrete result (eliminated ETL flows, reduced OpEx, full lineage).
|
||||
6. **CTA** - single call to action, typically "book a Discovery Call - link in the comments below 👇". LinkedIn deprioritises posts with external links, so put URLs in the first comment, not the body.
|
||||
|
||||
## Formatting capabilities and tricks
|
||||
|
||||
LinkedIn posts are plain text - there is no rich-text editor, so **no markdown**. Bold, italics and other styling you see in posts are faked with Unicode characters:
|
||||
|
||||
- **Faux bold / italic / monospace via Unicode Mathematical Alphanumeric Symbols** - e.g. `𝐛𝐨𝐥𝐝` (U+1D401+), `𝑖𝑡𝑎𝑙𝑖𝑐` (U+1D456+), `𝚖𝚘𝚗𝚘𝚜𝚙𝚊𝚌𝚎` (U+1D670+). Tools like YayText / LingoJam generate these. **Use with extreme restraint:** they break screen readers and accessibility tools, are not searchable (LinkedIn search treats them as different characters, so keywords in faux-bold won't be found), and can render as boxes on some devices. Our default: avoid them; use CAPS sparingly for emphasis instead. If a hook or single key term really needs to pop, one short faux-bold phrase is the acceptable ceiling.
|
||||
- **Bullets** - no native lists; use literal characters: `-`, `•`, `✓`, `→`, numbered `1.` etc. Each on its own line.
|
||||
- **Whitespace** - blank lines survive and are the primary layout tool; use them aggressively (1-3 sentence paragraphs). LinkedIn collapses multiple blank lines beyond one, so a single blank line is the unit.
|
||||
- **Emoji** - work natively and render fine; use sparingly and professionally (👇 for the CTA, occasional ✅). Avoid emoji walls.
|
||||
- **Links** - URLs in the body get auto-linked but LinkedIn deprioritises posts containing external links; put the URL in the first comment and point to it ("link in the comments 👇"). Tag people/companies with `@mention` (native, drives reach) where relevant.
|
||||
- **Truncation** - only the first ~3 lines (~210 characters on mobile) show before "…see more". The hook must earn the click; never waste the first line on a greeting or context-setting.
|
||||
- **Special characters** - Unicode symbols like → ✅ ⏱ work fine and can structure a post; avoid exotic characters that some fonts render as tofu (□).
|
||||
- **No nested structure** - no headings, tables, quotes or code blocks. Simulated section breaks (a line of `—` or just blank space) are the only separators.
|
||||
|
||||
## Formatting rules
|
||||
|
||||
- **Hashtags are written as `#tag`** (e.g. `#endusercomputing`, `#excel`, `#datagovernance`, `#sas`, `#masterdata`). Never write `hashtag#tag` - that is an artefact of copy-pasting from LinkedIn's editor UI and must be stripped/normalised to `#tag` before posting or republishing.
|
||||
- 3-5 hashtags, all lowercase, placed at the end on their own lines.
|
||||
- Use regular dashes (`-`), not em-dashes (`—`).
|
||||
- Short paragraphs (1-3 sentences) with blank lines between them - LinkedIn has no markdown.
|
||||
- No markdown syntax (no `**bold**`, no `[links](url)`) - LinkedIn renders plain text. Use CAPS sparingly for emphasis instead.
|
||||
- ® symbol on first mention of SAS® is fine; don't repeat it every time.
|
||||
- Keep total length under ~1,300 characters where possible; longer posts are fine when the story warrants it, but the hook still has to earn the "see more" click.
|
||||
|
||||
## Media
|
||||
|
||||
- Always attach an image - posts with images get substantially more reach. Use the same image as the feed post's `previewImg` where one exists.
|
||||
- **Optimum image size: 1200 x 627 px (1.91:1 landscape)** - LinkedIn's recommended size for feed images and link previews, displays uncropped on desktop and mobile. Square (1080 x 1080) and portrait (1080 x 1350, 4:5) also work and take up more feed real estate on mobile; avoid narrow/tall images beyond 4:5 as they get cropped. Keep key content away from the edges.
|
||||
- Product screenshots work well for feature announcements; flat branded illustrations for thought-leadership posts.
|
||||
|
||||
## Relationship to the site Feed
|
||||
|
||||
- Feed posts (`content/feed/`) and LinkedIn posts are siblings, not copies: the site version may contain markdown links and omits hashtags; the LinkedIn version uses plain text, `#tag` hashtags, and "link in the comments".
|
||||
- When converting site → LinkedIn: strip markdown, convert links to a comment-based CTA, append hashtags.
|
||||
- When converting LinkedIn → site: strip `hashtag#` artefacts and hashtags, add proper markdown links, add front matter per the `add-feed-post` skill.
|
||||
- **Keep them in sync:** whenever a feed post is modified, update its `Source LinkedIn post:` comment in the same change (same points, same claims, same ordering of lists) - and vice versa. A stale LinkedIn comment means the next social post is drafted from outdated copy.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Data Controller - Sales & Positioning Playbook
|
||||
|
||||
Reference for writing marketing, blog, feed, and sales copy. Covers pain points, target audience, value proposition, packaging, sales motion, and objection handling. Synthesised from the marketing site (`datacontroller.io`), product docs (`docs.datacontroller.io`), and product glossary (`dc/CONTEXT.md`).
|
||||
|
||||
## The product in one paragraph
|
||||
|
||||
Data Controller for SAS® is a SAS-Powered web application that lets business users safely add, modify and delete data in SAS datasets and databases. Every change goes through an edit-stage-approve workflow: edits are staged, validated at the point of entry, reviewed as a diff (new / changed / deleted rows), approved by a data owner, then loaded - with a full audit trail of who changed what, when, and why. It runs on SAS Viya, SAS 9 EBI and SASjs Server, entirely on-prem / offline (no external network calls). It can write to any database reachable via a SAS libname engine.
|
||||
|
||||
## The problem (pain points)
|
||||
|
||||
SAS users routinely need to load or fix data manually - steering tables, parameters, mappings, corrections, regulatory adjustments. The status-quo approaches all hurt:
|
||||
|
||||
- **Network drive + batch job** - drop a CSV/Excel on a share, wait for a nightly job. Every new input means a new job to build, test, document and deploy. File-format drift breaks batches at the worst times (month end).
|
||||
- **Direct SAS code / Enterprise Guide edits** - requires granting end users MODIFY access to sensitive data locations. High risk of locking datasets, no audit trail, no approval.
|
||||
- **Asking a DBA / IT to run a script** - change requests, queues, delays, blame games between business and IT.
|
||||
- **Bespoke in-house capture apps** - years of development and testing, and a "black box" nobody wants to maintain.
|
||||
- **EUC sprawl** - uncontrolled Excel/VBA applications on network shares: opaque access, no version control, no lineage, audit failure. Regulators care (SOX, BCBS, national DQ circulars). Fines are real: Morgan Stanley $5m (2020), Citibank $400m (2020), DTCC £350k (2021).
|
||||
|
||||
The five measurable cost areas (see blog post "ROI and Payback"):
|
||||
|
||||
1. **Development time** - reduced to zero (zero-code, config-driven; the grid is generated dynamically from table metadata).
|
||||
2. **Deployment time** - reduced to zero (config change in the UI, not a code release).
|
||||
3. **Batch incidents** - invalid data cannot be submitted (validated on arrival).
|
||||
4. **Data quality issues** - schema-based automatic rules plus configurable frontend validations and backend hook scripts.
|
||||
5. **Compliance costs** - single source of truth, full audit trail, Excel originals retained with each submission; no reperformed audits of unsecured EUC code.
|
||||
|
||||
## Target audience
|
||||
|
||||
**Verticals:** regulated industries with large SAS estates - Banking, Insurance, Pharma / Life Sciences. Typical use cases: regulatory reporting adjustments, actuarial / bitemporal reporting (Allianz), DWH steering-table management (AFA försäkring), data catalogs and lineage (Siemens Healthineers), BICC / BI competency centres (DER Touristik).
|
||||
|
||||
**Personas:**
|
||||
|
||||
- **Economic buyer:** Head of BI / Data Platform owner, CDO, Head of Regulatory Reporting. Cares about OpEx, risk reduction, audit outcomes, compliance.
|
||||
- **Champion:** SAS platform owner / technical manager drowning in ad-hoc data-load requests.
|
||||
- **End users:** business users (Editors) submitting data; data owners (Approvers) signing off; Auditors reviewing history. Roles: Viewer, Editor, Approver, Auditor, Administrator.
|
||||
- **Gatekeepers:** IT security (satisfied by: on-prem, no external calls, uses existing SASLogon auth, row & column level security, end users never get direct write access to target tables).
|
||||
|
||||
**Audience tone (from the LinkedIn guide):** assume competence - don't explain ETL or data warehouses. Speak to their pains: reporting deadlines, audit trails, hand-cranked ETL, EUC risk, OpEx. Professional and direct; no hype.
|
||||
|
||||
## Value proposition / key messages
|
||||
|
||||
- **Zero code** - adding a new editable table is configuration, done in the tool itself. Column types, lengths, dropdowns are derived from the target table at runtime.
|
||||
- **Review / approve workflow** - nothing is applied without 1+ approvals; approvers see only the diff.
|
||||
- **Full audit history** - who, when, why, what; original uploaded files retained.
|
||||
- **Secure** - existing SAS authentication; RLS and CLS enforced server-side; no end-user write access to targets.
|
||||
- **Real-time** - intra-day concurrent updates via lock table and queuing; loads abort if the table changed since the diff.
|
||||
- **Any database** - via SAS libname engines; supports retained keys, composite keys, SCD2 (TXTEMPORAL) and full BITEMPORAL history.
|
||||
- **Validation at point of entry** - HARDREGEX/SOFTREGEX, dropdowns, min/max, NOT NULL, plus hook scripts to run arbitrary SAS programs before/after edit or approve.
|
||||
- **Saves vs alternatives** - years of dev/test vs bespoke build; no black box.
|
||||
|
||||
## Packaging & pricing
|
||||
|
||||
Four packages, all with **unlimited users** (since v6), all flavours of SAS (Viya / EBI / SASjs Server):
|
||||
|
||||
| Package | Price | Shape |
|
||||
| --- | --- | --- |
|
||||
| Community | FREE | 35 tables/library, 15 view rows, 5 upload rows, 1 viewbox, best-endeavours support |
|
||||
| Bronze | 12k EUR pa | 1 production env, 100 tables/library, 100 rows, 8hr SLA, SASjs support |
|
||||
| Silver | 21k EUR pa | 2 production envs, unlimited tables/rows, 6 viewboxes, in-house SAS app support |
|
||||
| Gold | Contact us | Unlimited envs, 4hr SLA, developer + consulting days, **perpetual licence option** |
|
||||
|
||||
- Add-on: Developer Days (new product features) at 600 EUR/day for all plans; consulting rates at sasapps.io/pricing.
|
||||
- Pricing goals: Transparent, Simple, Fair. Full public price list on the site - a differentiator; use it.
|
||||
- Support: tickets via support.4gl.io or support@datacontroller.io, severity levels 1-4, SLAs per package, escalation by video call between managers.
|
||||
|
||||
## Sales motion / execution strategy
|
||||
|
||||
1. **Land with the free tier.** The Community package and the evaluation SPK (single-file import, ~minutes to install on SAS 9) make trying the product nearly frictionless. Since v6.1 the source is also available for anyone to build and evaluate. Push evaluation hard - it is the top of funnel. The eval version is deliberately not production-grade (BASE-engine config tables, table-lock risk, unlicensed interface) - the natural upgrade path is built in.
|
||||
2. **Discovery call as the primary CTA.** Marketing (LinkedIn, blog, feed) always ends with "book a Discovery Call". Content strategy: hook with the pain (EUC risk, ETL sprawl, audit pain), not the product.
|
||||
3. **Expand from one use case.** Typical wedge: steering tables / mapping tables / parameter tables in a reporting team. Expansion levers: more tables, more environments, more business units, API-driven submissions (API Explorer since v6).
|
||||
4. **Sell the business case, not features.** Use the ROI/Payback blog post (five cost areas + calculator) so the champion can build an internal business case. Quantify dev hours, batch incidents, audit effort.
|
||||
5. **Partner channel.** Resellers exist (e.g. SaasNow in the Netherlands); partner-purchased deals may have partner first-line support.
|
||||
6. **Roadmap as a sales tool.** The public roadmap is customer-driven - "features requested by customers get built" - and Gold includes Developer Days. Position the product as one that listens.
|
||||
7. **Proof points.** Named customer stories (AFA, Allianz, Siemens Healthineers, DER Touristik), SAS 2021 Excellence in Innovation Award finalist.
|
||||
|
||||
## Getting it installed (technical ease-of-adoption)
|
||||
|
||||
- Deploys entirely inside the customer's existing SAS platform - no new infrastructure, no external assets or network calls at runtime (passes strict security reviews).
|
||||
- SAS 9 EBI: SPK import via SAS Management Console / DI Studio; run the configurator stored process; done. Requirements documented at docs.datacontroller.io (`dci-requirements`, `dci-deploysas9`, `dci-stpinstance`).
|
||||
- Viya and SASjs Server deployments also documented (`deploy-viya`).
|
||||
- Instillation/deployment support is in-scope for all paid support plans.
|
||||
|
||||
## Licensing
|
||||
|
||||
- Annual subscription per package (Bronze/Silver/Gold); perpetual licence available at Gold.
|
||||
- Community tier is free; the evaluation build is governed by an evaluation agreement (Bowe IO Ltd) - evaluation/non-production use only, no redistribution, no reverse engineering, "as is".
|
||||
- Embedded Handsontable requires a licence key for commercial use - handled as part of paid deployments.
|
||||
|
||||
## Objection handling (from the FAQ)
|
||||
|
||||
- **"Can it change our data model?"** - No; it updates data, not structure. Positioned as a safety feature.
|
||||
- **"Our preferred database?"** - Yes, any engine SAS can reach (incl. Snowflake, Redshift support announcements).
|
||||
- **"Retained / surrogate keys?"** - Supported, along with composite primary keys, SCD2 and bitemporal loads.
|
||||
- **"Security review will block it."** - On-prem, offline, existing SASLogon, RLS/CLS, no direct target access, no metadata modification after install.
|
||||
- **"We could build this."** - Yes, at the cost of years of dev/test plus an unmaintainable black box; DC is proven, supported, documented, and source-available for evaluation.
|
||||
|
||||
## Copy rules when writing sales-adjacent content
|
||||
|
||||
- Regular dashes, not em-dashes. No hard-wrapped Markdown.
|
||||
- Use product glossary terms exactly (submission, staging, target table, load type, etc. - see `dc/CONTEXT.md`).
|
||||
- ® on first mention of SAS®.
|
||||
- CTA conventions per channel: site content may link directly; LinkedIn puts links in the first comment (see `.agents/docs/linkedin.md`).
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: add-feed-post
|
||||
description: Add a new Feed post (short announcement / social-style update) to the Data Controller marketing site. Use when the user wants to publish a Feed post, add a /feed/ entry, or create a short announcement that also goes out via RSS.
|
||||
---
|
||||
|
||||
# Add a Feed post
|
||||
|
||||
The **Feed** section (`/feed/`) is for short announcements and social-media style updates - things that don't warrant a full `content/blog/` article but are still worth publishing and syndicating via RSS. Feed posts work exactly like blog posts (one markdown file per post, in its own directory) but live under `content/feed/` and publish under `/feed/`.
|
||||
|
||||
Both Blog and Feed share a single combined RSS feed at `/rss.xml` (via `gatsby-plugin-feed` in `gatsby-config.js`), so a new Feed post appears in RSS automatically on the next build - no extra config.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Create the post directory
|
||||
|
||||
Create a folder under `content/feed/`, named after the post slug (this becomes the URL):
|
||||
|
||||
```
|
||||
content/feed/my-new-announcement/index.md
|
||||
```
|
||||
|
||||
Pick a short, hyphenated, lowercase slug.
|
||||
|
||||
### 2. Write the front matter + content
|
||||
|
||||
```md
|
||||
---
|
||||
title: 'My New Announcement'
|
||||
description: A one or two sentence summary used for SEO and RSS.
|
||||
date: '2024-06-01 09:00:00'
|
||||
author: 'Your Name'
|
||||
authorLink: https://www.linkedin.com/in/yourprofile/
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
Your announcement content goes here, written in regular Markdown.
|
||||
```
|
||||
|
||||
Front matter fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| ------------- | -------- | --------------------------------------------------------------------- |
|
||||
| `title` | Yes | Used on the page, in the sidebar and in the RSS item. |
|
||||
| `description` | Yes | Short summary for SEO meta tags and the RSS item description. |
|
||||
| `date` | Yes | Format `'YYYY-MM-DD HH:MM:SS'`. Controls sort order and the archive. |
|
||||
| `author` | Yes | Displayed under the post title. |
|
||||
| `authorLink` | No | If set, the author name links out (e.g. to a LinkedIn profile). |
|
||||
| `tags` | Yes | One or more tags. Drives the sidebar category list and RSS category. |
|
||||
| `previewImg` | No | Relative path to an image in the same folder (e.g. `'./cover.png'`). The template renders this image on the post page automatically - do NOT also embed the same image in the markdown body. |
|
||||
|
||||
Use regular dashes (`-`) in content, not em-dashes.
|
||||
|
||||
If the post is adapted from source copy the user provided (e.g. a LinkedIn post), paste the original text into an HTML comment (verbatim, except strip the literal `hashtag` prefix that LinkedIn sometimes adds before `#tags`, e.g. `hashtag#sas` -> `#sas`) (`<!-- ... -->`) at the bottom of the markdown file, labelled (e.g. `Source LinkedIn post:`). This preserves the original - including hashtags and checkmarks that get dropped/adapted in the site version - for future reuse.
|
||||
|
||||
**Important:** when editing an existing feed post that has a `Source LinkedIn post:` comment, ALWAYS update the LinkedIn version too, so the overall message stays consistent (same points, same claims, same ordering of any lists). Wording does not need to be identical, and formatting will differ: the article uses Markdown, while LinkedIn is plain text with `✓`-style bullets, hashtags, and a comment-based CTA (see `.agents/docs/linkedin.md`).
|
||||
|
||||
Every new feed post should include an image (a `previewImg` cover) - and its full image-generation prompt must be recorded in the same HTML comment block, labelled (e.g. `Image prompt:`), so it can be tweaked and regenerated later. If the user has not provided or asked for an image, propose one (theme, style and a draft prompt) and add the `Image prompt:` comment even if the image itself is not yet generated.
|
||||
|
||||
### 3. Add images (optional)
|
||||
|
||||
If you set `previewImg` or reference images in the body, place the image files in the same post directory and reference them with a relative path.
|
||||
|
||||
### 4. Build / preview
|
||||
|
||||
```shell
|
||||
npm run develop
|
||||
```
|
||||
|
||||
The post will be available at `/my-new-announcement/` and will appear on the `/feed/` listing page, in the site search index, and in `/rss.xml` alongside blog posts.
|
||||
|
||||
## How Feed differs from Blog
|
||||
|
||||
| Aspect | Blog | Feed |
|
||||
| ----------------- | ----------------------------- | --------------------------------- |
|
||||
| Content directory | `content/blog/` | `content/feed/` |
|
||||
| Listing page | `/blog/` | `/feed/` |
|
||||
| Year archive | `/{year}/` | `/feed/{year}/` |
|
||||
| Category page | `/category/{tag}/` | `/feed/category/{tag}/` |
|
||||
| Template | `src/templates/blog-post.tsx`, `src/templates/blog-list.tsx` | `src/templates/feed-post.tsx`, `src/templates/feed-list.tsx` |
|
||||
| RSS | Included in `/rss.xml` | Included in `/rss.xml` |
|
||||
|
||||
Feed archive/category routes are prefixed with `/feed/` to avoid colliding with the equivalent Blog routes, since both are rendered by the shared `src/templates/sidebar.tsx` component (via its `basePath` prop).
|
||||
|
||||
## Notes
|
||||
|
||||
- To add a **blog** post instead, follow the same steps but use `content/blog/` and the `/blog/` routes.
|
||||
- The canonical human-readable version of this guide is `docs/adding-feed-posts.md`.
|
||||
@@ -10,20 +10,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
npm -g install cloudron-surfer
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
run: npx surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
@@ -1,26 +1,25 @@
|
||||
name: Gatsby Publish
|
||||
|
||||
# on:
|
||||
# push:
|
||||
# branches: main
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.17.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
- name: Use Node.js from .nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Check prettier offences
|
||||
run: npm run lint
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
legacy-peer-deps=true
|
||||
ignore-scripts=true
|
||||
save-exact=true
|
||||
fund=false
|
||||
@@ -0,0 +1,34 @@
|
||||
## Related repositories
|
||||
|
||||
This is the Data Controller **marketing site, blog and feed** (Gatsby). It has two sibling repos, usually checked out side by side:
|
||||
|
||||
- **`dc`** - the product source (Angular client + SAS backend). Behaviour of the product itself is defined there; see its `CONTEXT.md` for the domain glossary.
|
||||
- **`docs.datacontroller.io`** - the user-facing product documentation (MkDocs), published at docs.datacontroller.io.
|
||||
|
||||
When writing marketing/blog/feed copy about a feature, treat `docs.datacontroller.io` and `dc` as the source of truth for how the product actually behaves.
|
||||
|
||||
## Content structure
|
||||
|
||||
- **Blog** (`content/blog/`) - long-form articles, published at `/blog/`.
|
||||
- **Feed** (`content/feed/`) - short announcements, published at `/feed/`.
|
||||
- Both are combined into a single RSS feed at `/rss.xml`.
|
||||
- To add a feed post, use the `add-feed-post` skill (`.agents/skills/add-feed-post/SKILL.md`).
|
||||
|
||||
## Agent files
|
||||
|
||||
All agent-facing material lives under `.agents/`:
|
||||
|
||||
- `.agents/skills/` - pi-discovered skills (e.g. `add-feed-post`).
|
||||
- `.agents/docs/` - reference docs consumed by skills and these instructions. Notably `sales.md` (product positioning, pain points, audience, pricing and sales motion) - consult it when writing marketing, blog, feed or sales copy.
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues live as markdown files under `.scratch/`. See `.agents/docs/issue-tracker.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context layout - `CONTEXT.md` at the repo root + ADRs in `.agents/adr/`. See `.agents/docs/domain.md`.
|
||||
|
||||
### Writing style
|
||||
|
||||
Use regular dashes (`-`) in content, not em-dashes (`—`).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Context: datacontroller.io (marketing site)
|
||||
|
||||
The public marketing website for Data Controller for SAS®, built with **Gatsby** (React + TypeScript, styled-components). It hosts the product's marketing pages, blog and feed. It is separate from the product itself (`dc`) and the product documentation (`docs.datacontroller.io`).
|
||||
|
||||
## What the product is
|
||||
|
||||
Data Controller for SAS® is a web application that lets users safely add, modify and delete data in SAS datasets and databases, with an edit-stage-approve workflow and full change history. It runs on SAS Viya, SAS 9 EBI and SASjs Server. For the product's own domain vocabulary (roles, load types, MPE control tables, validations, security), see `dc/CONTEXT.md`; for user-facing feature docs, see the `docs.datacontroller.io` repo. Treat those as the source of truth when writing copy about how the product behaves.
|
||||
|
||||
## Layout
|
||||
|
||||
- `content/blog/` - long-form articles (published at `/blog/`).
|
||||
- `content/feed/` - short announcements (published at `/feed/`). See the `add-feed-post` skill.
|
||||
- `src/pages/` - top-level pages (`index`, `about`, `contact`, `faq`, plus the markdown-driven `{MarkdownRemark.frontmatter__slug}.tsx`).
|
||||
- `src/markdown-pages/` - standalone markdown pages (e.g. `pricing.md`).
|
||||
- `src/templates/` - list/post/sidebar templates for blog and feed.
|
||||
- `src/components/` - shared React components (layout, navibar, footer, hero, seo).
|
||||
- `static/` - static assets served as-is; `gatsby-*.js` - Gatsby config (feed, node APIs, etc.).
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use regular dashes (`-`) in content, not em-dashes.
|
||||
- Blog and feed share a single combined RSS feed at `/rss.xml` (via `gatsby-plugin-feed`); new posts appear automatically on the next build.
|
||||
- Lint with `npm run lint` (prettier) and typecheck with `npm run typecheck` before considering front-end changes done.
|
||||
- Preview locally with `npm run develop`.
|
||||
|
||||
## Agent files
|
||||
|
||||
All agent-facing material lives under `.agents/`: skills in `.agents/skills/` (e.g. `add-feed-post`), and reference docs in `.agents/docs/` (`domain.md`, `issue-tracker.md`).
|
||||
|
||||
See `AGENTS.md` for agent skills, writing rules, and where these files live.
|
||||
@@ -30,3 +30,14 @@
|
||||
|
||||
- <img class="alignright" src="/wp-content/uploads/2021/04/2IrsV7v.png" alt="Title" width="352" height="442" />
|
||||
- <img class="alignleft" src="/wp-content/uploads/2021/04/2IrsV7v.png" alt="Title" width="352" height="442" />
|
||||
|
||||
## 📰 Blog & Feed content
|
||||
|
||||
There are two markdown-driven content sections on this site:
|
||||
|
||||
- **Blog** (`content/blog/`) — long-form articles, product releases and case studies. Published at `/blog/`.
|
||||
- **Feed** (`content/feed/`) — short announcements and social-media style updates. Published at `/feed/`.
|
||||
|
||||
Both sections are combined into a single RSS feed at [`/rss.xml`](https://datacontroller.io/rss.xml).
|
||||
|
||||
See [`docs/adding-feed-posts.md`](docs/adding-feed-posts.md) for a step-by-step guide on adding a new Feed post.
|
||||
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Data Controller - 2024 Recap
|
||||
description: A recap of the features delivered in Data Controller for SAS over the 24 releases of 2024
|
||||
date: '2024-12-31 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './darkmode.png'
|
||||
tags:
|
||||
- Data Controller
|
||||
- SAS
|
||||
- Excel
|
||||
- Recap
|
||||
---
|
||||
|
||||
Today marks the last day of 2024 and another period of growth for Data Controller for SAS. We made **24** [releases](https://git.datacontroller.io/dc/dc/releases) over the last 12 months, moving from [v6.3.1](https://git.datacontroller.io/dc/dc/releases/tag/v6.3.1) to [v6.12.1](https://git.datacontroller.io/dc/dc/releases/tag/v6.12.1). We also on-boarded three new clients - two banks, and an insurance company. Every single one of our existing customers renewed. And some even invested in some new features. In this article we'll go through the major additions made during 2024.
|
||||
|
||||
## Complex Excel Uploads
|
||||
|
||||
A Basel III regulatory workstream necessitated ingestion of a series of Quantitative Tables as well as data from other workbooks, that were particularly large and packed with data fields situated in dynamic locations within the workbook.
|
||||
|
||||
To meet the requirements (and more) we built a feature to allow the ingestion of multiple ranges, from multiple worksheets, from within an arbitrary workbook. The target range can be defined using either a fixed (A1) or dynamic (R1C1) notation. The sheets themselves may be referenced by name, or position.
|
||||
|
||||
By way of example, consider the ingestion of the yellow squares below:
|
||||
|
||||

|
||||
|
||||
One potential configuration (of many) to upload the above data would be as follows (extract from [MPE_XLMAP_RULES](/tables/mpe_xlmap_rules) table):
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
The following video illustrates:
|
||||
|
||||
<iframe title="Complex Excel Uploads" width="560" height="315" src="https://vid.4gl.io/videos/embed/3338f448-e92d-4822-b3ec-7f6d7530dfc8" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
|
||||
## Filter By Variable
|
||||
|
||||
Since [v6.5](https://git.datacontroller.io/dc/dc/releases/tag/v6.5.0) it is now possible to use variables instead of _values_ on the right hand side of a query expression. This provides more flexibility in creating filters.
|
||||
|
||||

|
||||
|
||||
## Data Restore
|
||||
|
||||
Since adding the [audit](https://docs.datacontroller.io/tables/mpe_audit/) data last year, it is now possible (since [v6.8](https://git.datacontroller.io/dc/dc/releases/tag/v6.8.0)) to [restore](https://docs.datacontroller.io/restore/) a table to a particular previous version!
|
||||
|
||||
This works by navigating first to the submission of the version to be restored, and hitting the red REVERT button. This will make a NEW submission with the changes that need to be made to revert the values to the previous state (which needs to be approved in the usual fashion).
|
||||
|
||||

|
||||
|
||||
## Accessibility Updates
|
||||
|
||||
It's a hard requirement that Data Controller provides the necessary accessibility adjustments (eg Dark Mode, keyboard shortcuts, contrast / font size settings etc) to accomodate users with particular needs (eg section 508 and EAA). Thanks to our HandsOnTable licence, the DC data grid will even support for screen readers, such as NVDA, Jaws (Windows) and VoiceOver (MacOS).
|
||||
|
||||

|
||||
|
||||
<iframe title="Data Controller Accessibility" width="560" height="315" src="https://vid.4gl.io/videos/embed/b3d7e820-249f-4bc2-9e1a-473ce90a7a96" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
## Multiload Feature
|
||||
|
||||
For a long time we have supported the load of a single (rectangular) table from anywhere in a workbook. Well, as of [v6.11](https://git.datacontroller.io/dc/dc/compare/v6.10.1...v6.11.0) you can now load _multiple_ tables from a single workbook, significantly reducing the effort of manual uploads. This piece of work also resulted in a big re-write of our excel extraction logic, which is now 20x faster than before.
|
||||
|
||||

|
||||
|
||||
|
||||
------------
|
||||
|
||||
We would like to thank all of our customers for their enthusiasm for and support with our product. Our goal is to make it secure, simple, and fast to load ad-hoc data into SAS.
|
||||
|
||||
If you'd like to kick the tyres, [reach out](https://datacontroller.io/contact/), or just go ahead and perform an [installation](https://docs.datacontroller.io/dci-requirements/) - the base product is completely free to use, with [premium features](https://datacontroller.io/pricing/) / support plan available on request.
|
||||
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Data Controller - 2025 Recap
|
||||
description: A recap of the features delivered in Data Controller for SAS over the 34 releases of 2025
|
||||
date: '2025-12-31 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './releases2025.png'
|
||||
tags:
|
||||
- Data Controller
|
||||
- SAS
|
||||
- Recap
|
||||
---
|
||||
|
||||
In 2025, Data Controller’s theme was **making governed, auditable data change easier to roll out** — especially on SAS Viya — while continuing to invest in metadata/catalog capabilities and accessibility.
|
||||
|
||||
## What is Data Controller?
|
||||
|
||||
Data Controller is a SAS-powered web application for **adding, modifying, and deleting data** in target tables—while keeping control with data owners via a **review/approve workflow** and a **full audit trail**.
|
||||
|
||||
Under the hood, it’s designed for multi-user, production-friendly operation (locking, security, approvals, and repeatable update logic), and it supports multiple SAS environments (including SAS 9 and Viya).
|
||||
|
||||

|
||||
|
||||
## 2025 at a glance
|
||||
|
||||
Across calendar year 2025, [releases](https://git.datacontroller.io/dc/dc/releases) moved from **v6.12.2 (27JAN25)** to **v7.2.5 (09DEC25)**.
|
||||
|
||||
Key themes:
|
||||
|
||||
- **Viya deployments became dramatically simpler**, including "streaming app" deployment improvements (frontend in SAS Files), sensible defaults, and compute-context-aware setup.
|
||||
- **Data Catalog expanded** to include SAS catalogs and associated objects / entries, with corresponding storage changes and migrations.
|
||||
- **Accessibility work accelerated**, culminating in high Lighthouse scores, improved screen reader support, and CI checks to keep it that way.
|
||||
|
||||
## 1) Viya: from "deployment project" to "quick start"
|
||||
|
||||
A major through-line of 2025 was reducing the friction of getting DC live on SAS Viya:
|
||||
|
||||
- **Content Security Policy (CSP) hardening & UI refactor**
|
||||
The UI was refactored to comply with Viya’s default (and strict) CSP, with deploy-flow improvements targeted at Viya.
|
||||
|
||||
- **Better defaults for single-user installs**
|
||||
DC began defaulting the physical deployment path and SAS Drive location in ways that reduce the need for pre-provisioned shared directories in straightforward scenarios.
|
||||
|
||||
- **Compute-context-aware installs & streaming deploy maturity**
|
||||
Releases in early June focused on ensuring deployment actions happen with the selected shared compute context, and then delivering a **streaming Viya version** that can self-configure key deployment choices during setup.
|
||||
|
||||
## 2) Extending the Data Catalog: SAS catalogs + objects
|
||||
|
||||
On **June 11, 2025**, **v7.0.0** introduced a major upgrade to cataloging:
|
||||
|
||||
- Explore **SAS catalogs** and their **objects**, including created/modified attributes
|
||||
- Store catalog + object data alongside existing catalog entities (libraries/tables/variables)
|
||||
- UX improvements like additional counts and reducing the need for manual refresh steps after table configuration changes
|
||||
|
||||
This aligned well with DC’s broader approach to metadata: DC’s **optional** data + catalog scan is designed to track how your SAS estate changes over time (including storing history using SCD2 patterns).
|
||||
|
||||
## 3) Accessibility + UX: measurable improvements (and staying power)
|
||||
|
||||
2025 saw repeated, explicit investment in accessibility:
|
||||
|
||||
- **v6.14.5:** "significantly improves accessibility (lighthouse) scores" and also included a practical SAS hook-related robustness fix (`%GLOBAL SQLRC;`) for embedded DI Job usage.
|
||||
- **v7.1.0:** a frontend-only release focused on accessibility (Lighthouse 100%) and screen reader support, plus a small grid interaction improvement (Excel-like deselection).
|
||||
- **v7.2.0:** introduced a **Lighthouse accessibility check pipeline** to keep accessibility from regressing.
|
||||
|
||||
## 4) Internationalisation + "real world" data correctness
|
||||
|
||||
A few 2025 changes were about getting the "last mile" right in global SAS estates:
|
||||
|
||||
- Handling national-language datetime formats
|
||||
- Metadata navigation sorting improvements using linguistic sort sequences
|
||||
|
||||
## 5) A 2025 customer story: AFA Insurance
|
||||
|
||||
During 2025 we also [detailed the Data Controller use case with AFA Insurance (Sweden)](https://datacontroller.io/afa-insurance). They describe using Data Controller primarily for **steering/mapping/control tables**, where:
|
||||
|
||||
- End users perform the mapping themselves in DC
|
||||
- Approvals are done directly in production without needing fixed programs
|
||||
- Filters are used so users focus only on "new signals" that need mapping
|
||||
|
||||
They call out the **approval workflow** and **history/audit** as standout features, and also mention value from dropdowns, hooks, and lineage.
|
||||
|
||||
## 2025 Release Highlights
|
||||
|
||||

|
||||
|
||||
------------
|
||||
|
||||
We would like to thank all of our customers for their continued support and feedback.
|
||||
|
||||
> Our mission is for Data Controller to be secure, simple, and fast - when loading ad-hoc data into SAS.
|
||||
|
||||
If you'd like to try it out, just go ahead and perform an [installation](https://docs.datacontroller.io/dci-requirements/) - the base product does not require a paid license.
|
||||
|
||||
We're also happy to provide complementary deployment support, just [reach out](https://datacontroller.io/contact/).
|
||||
|
||||
<!--
|
||||
|
||||
## Selected 2025 release timeline (highlights)
|
||||
|
||||
| Date | Version | Theme | What changed |
|
||||
|---|---:|---|---|
|
||||
| 2025-01-31 | 6.13.0 | Quality | CSV upload/editor fixes + related tests |
|
||||
| 2025-03-24 | 6.14.5 | Accessibility | Lighthouse/accessibility improvements; hook robustness in DI-job contexts |
|
||||
| 2025-05-28 | 6.14.8 | Viya hardening | CSP-compliant UI refactor + Viya deploy flow improvements |
|
||||
| 2025-06-06 | 6.16.2 | Viya deploy | Streaming Viya deploy with self-configuration improvements |
|
||||
| 2025-06-11 | 7.0.0 | Data Catalog | SAS catalogs + objects captured/stored; reduced manual refresh steps |
|
||||
| 2025-07-23 | 7.1.0 | Accessibility | Frontend-only: Lighthouse 100%, better screen reader support |
|
||||
| 2025-08-08 | 7.2.0 | Engineering | CI pipeline for Lighthouse accessibility checks |
|
||||
| 2025-12-09 | 7.2.5 | Reliability | Dependency bumps, improved testing, restore-check flexibility|
|
||||
|
||||
|
||||
-->
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: AFA Insurance and Data Controller for SAS®
|
||||
description: Data Controller for SAS helps the DWh team at AFA försäkring with their steering table management
|
||||
date: '2025-02-25 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './afa_logo.png'
|
||||
tags:
|
||||
- Use Cases
|
||||
---
|
||||
|
||||
|
||||
AFA försäkring is a well-known insurance provider in Sweden with a focus on collective agreements and occupational insurance.
|
||||
|
||||
AFA has leveraged SAS for data management and analytics for many years, and recently integrated Data Controller for SAS to enhance controls and audit reporting for a number of business processes.
|
||||
|
||||
We caught up with [Fredrik Englund](https://www.linkedin.com/in/fredrik-englund-7402031/) (FE) and [Henrik Forsell](https://www.linkedin.com/in/henrikforsell/) (HF) to learn more about their usage of this toolset.
|
||||
|
||||
| |
|
||||
|:--:|
|
||||
| *Fredrik Englund (FE)* |
|
||||
|
||||
|
||||
### Can you tell us a bit about yourself?
|
||||
(FE) We are the developers of our DWh, a group of around 10 people. My role is SAS Developer and System Architect.
|
||||
|
||||
### What do you use Data Controller for?
|
||||
Mainly for steering (mapping, control) tables. We use it in our handling system for insurance agents – we have a signal system, such that when an agent finishes a task, there is a signal to move forward to the next step in the process. We map the signals to different process steps and use that in our reports to follow up. The mapping of signals to process steps are made by end users themselves, using Data Controller, and we make the approvals directly in production without needing fixed programs or anything else. This is a great advantage for us.
|
||||
|
||||
We have batches running and checking if there are new signals, and we have set up Data Controller with a filter so that end users get only the new signals in the interface, and can just do the mapping and click OK.
|
||||
|
||||
So it is very easy for us now to do that process!
|
||||
|
||||
### Why are you using Data Controller, instead of another approach or tool?
|
||||
We heard about Data Controller from the community, and it seemed like a great fit for our use case - it’s SAS friendly, it’s built on SAS, and it’s easy to incorporate.
|
||||
|
||||
The things we really like are the EDIT and APPROVAL process, and the HISTORY of all changes that is built into the product.
|
||||
|
||||
### Is anyone else at AFA using Data Controller?
|
||||
Not right now but we plan to use it more. We have people in the process development team who need to change targets once a year or maybe more often. Right now they send us the new targets and we add them in the programs / update the Visual Analytics reports, but we plan to use steering / control tables for them, so they can update targets themselves in Data Controller. Then they can request to change them when they like, and we have control over the timing of when the changes are applied.
|
||||
|
||||
We also have some percentages (relating to payments) with levels that change during the year, and those users should also be able to change the limits themselves in the Data Controller interface.
|
||||
|
||||
| |
|
||||
|:--:|
|
||||
| *Henrik Forsell (HF)* |
|
||||
|
||||
|
||||
(HF) This takes us completely out of the equation – we don’t have to do the changes, it’s all on them, they own it.
|
||||
|
||||
(FE) Yes – their data, their controls. We can leave it to them from the development environment, so they are not dependent on us for any changes, so it’s really good.
|
||||
|
||||
(HF) So the users can own the data, all of these things, they can change it themselves, we don’t need to do anything once we set it up.
|
||||
|
||||
### How much time does this save you, say, on a monthly basis?
|
||||
(FE) It’s hard to say how much time/money it saves – it’s more about the quality, and the control, and the history. If they put in wrong mappings, targets, or levels – how much will it cost us to reverse the the batches, it’s very hard to say
|
||||
|
||||
(HF) It’s also about the fact that even if it’s only one number in one report, if we have to do the changes, we’d almost have to setup a small project; as in, a task that has to be checked, added into our test environment, run tests, ok it, and move on into production. So even though it’s a small thing that changed, the process around it takes a lot of work. One change might be 3-4 hours in total. But now with Data Controller, they can change it twice a day if they want, it’s up to them, it doesn’t affect our work at all.
|
||||
|
||||
(FE) If we compare it to the old solution, they did the changes directly in the base tables – in production – so if they added the wrong values it would crash. Which happened, a few times. So it’s hard to say but you can spend like 10 hours on finding the error, or maybe more.
|
||||
|
||||
(HF) Or having to do a rollback, and then you involve even more people
|
||||
|
||||
(FE) It’s much more about quality. It can save us 2-10 hours a month, hard to say.
|
||||
|
||||
### What are your favourite features of DC?
|
||||
(FE) The UI is simple to understand (in a good way). It’s very responsive, very quick. For us the APPROVAL part is the big thing, together with the history of all changes. It’s really good to see who changes what, and when. We have a few people who can do the approval, and they can check very thoroughly that it’s correct before they approve it. So that’s actually the big one.
|
||||
|
||||
Then another good / big thing is the [dropdowns](https://docs.datacontroller.io/dynamic-cell-dropdown/), such that you can provide only values that are valid. And you can do [HOOKS](https://docs.datacontroller.io/dcc-tables/#pre_edit_hook) to check the data, both after and before.
|
||||
|
||||
Another was the [data lineage](https://docs.datacontroller.io/dcu-lineage/), which we haven’t had before. Now we can have analytics users check the lineage. We didn’t expect that but it was a nice thing to have.
|
||||
|
||||
### Is there anything you’d like to add?
|
||||
(HF) We love the response time from the developers.
|
||||
|
||||
(FE) The support is fabulous. If we can have this from all of our vendors it would be a perfect world.
|
||||
|
||||
(HF) And it is very important. When we found something – no words – you are there. That helps a lot.
|
||||
|
||||
(FE) Also, we haven’t tried out all the [excel import](https://docs.datacontroller.io/excel/) features yet but we’d like to try that in some way, as there is a lot of data that exists in excel, and that can be a way for business users to import that data, and check it, and improve it. So - we have plans.
|
||||
|
||||
---
|
||||
The team behind Data Controller would like to say "STORT TACK" to Fredrik and Henrik for their kind words!
|
||||
|
||||
---
|
||||
|
||||
The previous article in this series is available [here](/allianz-insurance-data-controller-sas/).
|
||||
@@ -15,7 +15,7 @@ tags:
|
||||
|
||||
<h1>What problem does Data Controller for SAS® solve?</h1>
|
||||
|
||||
<div class="imgHolder alignright"><a href="https://www.linkedin.com/in/rgagor/"><img src="/wp-content/uploads/2020/07/IMG-20190430-WA0049.jpg" alt="Rafal Gagor - Veteran SAS Developer" width="180" height="180" /></a><div><span>Rafal Gagor - Veteran SAS Developer</span></div></div>
|
||||
<div class="imgHolder alignright"><a target="_blank" rel="noopener" href="https://www.linkedin.com/in/rgagor/"><img src="/wp-content/uploads/2020/07/IMG-20190430-WA0049.jpg" alt="Rafal Gagor - Veteran SAS Developer" width="180" height="180" /></a><div><span>Rafal Gagor - Veteran SAS Developer</span></div></div>
|
||||
|
||||
It's a question we get asked a lot, and so this is the first of a series of articles that explore real users and their actual use cases. We caught up with [Rafal Gagor](https://www.linkedin.com/in/rgagor/), a DI Developer with 2 decades of SAS and Financial Services experience, to get his impressions after using Data Controller for SAS on a client project.
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ tags:
|
||||
- Solvency II
|
||||
---
|
||||
|
||||
When applying financial regulations in the EU (such as Solvency II, Basel III or GDPR) it is common for Member States to maintain or introduce national provisions to further specify how such rules might be applied. The National Bank of Belgium (NBB) is no stranger to this, and releases a steady stream of circulars via their <a href="https://www.nbb.be/en/financial-oversight/general/news/circulars-and-communications">website</a>. The <a href="https://www.nbb.be/doc/cp/eng/2017/20171012_nbb_2017_27.pdf">circular</a> of 12th October 2017 (NBB_2017_27, Jan Smets) is particularly interesting as it lays out a number of concrete recommendations for Belgian financial institutions with regard to Data Quality - and stated that these should be applied to internal reporting processes as well as the prudential data submitted. This fact is well known by affected industry participants, who have already performed a self assessment for YE2017 and reviewed documentation expectations as part of the HY2018 submission. <h2>Quality of External Data</h2> The DQ requirements for reporting are described by the 6 <a href="https://www.nbb.be/doc/cp/eng/2017/20171012_nbb_2017_27_annex.pdf">dimensions</a> (Accuracy, Reliability, Completeness, Consistency, Plausibility, Timeliness), as well as the Data Quality Framework described by Patrick Hogan <a href="https://www.bankingsupervision.europa.eu/press/conferences/sup_rep_conf/shared/pdf/Item4_1_PatrickHogan.pdf">here</a> and <a href="https://www.bankingsupervision.europa.eu/press/conferences/sup_rep_conf/shared/pdf/2017/Data_quality_framework_tools_and_products.pdf">here</a>. There are a number of 'hard checks' implemented in OneGate as part of the XBRL submissions, which are kept up to date <a href="http://www.eba.europa.eu/risk-analysis-and-data/reporting-frameworks">here</a>. However, OneGate cannot be used as a validation tool - the regulators will be monitoring the <strong>reliability</strong> of submissions by comparing the magnitude of change between resubmissions! Not to mention the data <strong>plausibility</strong> (changes in submitted values over time). <h2>Data Quality Culture</h2> When it comes to internal processes, CRO's across Belgium must now demonstrate to accredited statutory auditors that they satisfy the 3 Principles of the circular (Governance, Technical Capacities, Process). A long list of action points are detailed - it's clear that a <em>lot</em> of documentation will be required to fulfil these obligations! And not only that - the documentation will need to be continually updated and maintained. It's fair to say that automated solutions have the potential to provide significant time & cost savings in this regard. <h2>Data Controller for SAS®</h2> The Data Controller is a web based solution for capturing data from users. Data Quality is applied at source, changes are routed through an approval process before being applied, and all updates are captured for subsequent audit. The tool provides evidence of compliance with NBB_2017_27 in the following ways: <h4>Separation of Roles for Data Preparation and Validation (principle 1.2)</h4> Data Controller differentiates between Editors (who provide the data) and Approvers (who sign it off). Editors stage data via the web interface, or by direct file upload. Approvers are then shown the new, changed, or deleted records - and can accept or reject the update. <a href="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.50.56.png"><img class="aligncenter wp-image-962" src="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.50.56.png" alt="" width="553" height="296" /></a>
|
||||
When applying financial regulations in the EU (such as Solvency II, Basel III or GDPR) it is common for Member States to maintain or introduce national provisions to further specify how such rules might be applied. The National Bank of Belgium (NBB) is no stranger to this, and releases a steady stream of circulars via their <a target="_blank" rel="noopener" href="https://www.nbb.be/en/financial-oversight/general/news/circulars-and-communications">website</a>. The <a target="_blank" rel="noopener" href="https://www.nbb.be/doc/cp/eng/2017/20171012_nbb_2017_27.pdf">circular</a> of 12th October 2017 (NBB_2017_27, Jan Smets) is particularly interesting as it lays out a number of concrete recommendations for Belgian financial institutions with regard to Data Quality - and stated that these should be applied to internal reporting processes as well as the prudential data submitted. This fact is well known by affected industry participants, who have already performed a self assessment for YE2017 and reviewed documentation expectations as part of the HY2018 submission. <h2>Quality of External Data</h2> The DQ requirements for reporting are described by the 6 <a target="_blank" rel="noopener" href="https://www.nbb.be/doc/cp/eng/2017/20171012_nbb_2017_27_annex.pdf">dimensions</a> (Accuracy, Reliability, Completeness, Consistency, Plausibility, Timeliness), as well as the Data Quality Framework described by Patrick Hogan <a target="_blank" rel="noopener" href="https://www.bankingsupervision.europa.eu/press/conferences/sup_rep_conf/shared/pdf/Item4_1_PatrickHogan.pdf">here</a> and <a target="_blank" rel="noopener" href="https://www.bankingsupervision.europa.eu/press/conferences/sup_rep_conf/shared/pdf/2017/Data_quality_framework_tools_and_products.pdf">here</a>. There are a number of 'hard checks' implemented in OneGate as part of the XBRL submissions, which are kept up to date <a target="_blank" rel="noopener" href="http://www.eba.europa.eu/risk-analysis-and-data/reporting-frameworks">here</a>. However, OneGate cannot be used as a validation tool - the regulators will be monitoring the <strong>reliability</strong> of submissions by comparing the magnitude of change between resubmissions! Not to mention the data <strong>plausibility</strong> (changes in submitted values over time). <h2>Data Quality Culture</h2> When it comes to internal processes, CRO's across Belgium must now demonstrate to accredited statutory auditors that they satisfy the 3 Principles of the circular (Governance, Technical Capacities, Process). A long list of action points are detailed - it's clear that a <em>lot</em> of documentation will be required to fulfil these obligations! And not only that - the documentation will need to be continually updated and maintained. It's fair to say that automated solutions have the potential to provide significant time & cost savings in this regard. <h2>Data Controller for SAS®</h2> The Data Controller is a web based solution for capturing data from users. Data Quality is applied at source, changes are routed through an approval process before being applied, and all updates are captured for subsequent audit. The tool provides evidence of compliance with NBB_2017_27 in the following ways: <h4>Separation of Roles for Data Preparation and Validation (principle 1.2)</h4> Data Controller differentiates between Editors (who provide the data) and Approvers (who sign it off). Editors stage data via the web interface, or by direct file upload. Approvers are then shown the new, changed, or deleted records - and can accept or reject the update. <a href="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.50.56.png"><img class="aligncenter wp-image-962" src="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.50.56.png" alt="" width="553" height="296" /></a>
|
||||
|
||||
<h4>Capacities established should ensure compliance in times of stress (principle 2.1)</h4>
|
||||
As an Enterprise tool, the Data Controller is as scalable and resilient as your existing SAS platform.
|
||||
|
||||
<h4>Capture of Errors and Inconsistencies (principle 2.2)</h4> Data Controller has a number of features to ensure timely detection of Data Quality issues at source (such as cell validation, post edit hook scripts, duplicate removals, rejection of data with missing columns, etc etc). Where errors do make it into the system, a full history is kept (logs, copies of files etc) for all uploads and approvals. Emails of such errors can be configured for follow up. <h4>Tools and Techniques for Information Management Should be Automated (principle 2.3)</h4> The Data Controller can be configured to execute specific .sas programs after data validation. This enables the development of a secure and <em>integrated</em> workflow, and helps companies to avoid the additional documentation penalties associated with "miscellaneous unconnected computer applications" and manual information processing. <a href="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.53.38.png"><img class="aligncenter wp-image-963" src="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.53.38.png" alt="" width="278" height="128" /></a> <h4>Periodic Review & Improvements (principles 2.4 and 3.4)</h4> The Data Controller is actively maintained with the specific aim to reduce the cost of compliance with regulations such as NBB_2017_27. Our <a href="https://slides.com/allanbowe/datacontroller/#/">roadmap</a> includes new features such as pre-canned reports, version 'signoff', and the ability to reinstate previous versions of data. <h4>A process for correction and final validation of reporting before submission (3.1)</h4> As a primary and dedicated tool for data corrections, Data Controller can be described once and used everywhere. <h4>List of Divisions Involved in Preparing Tables (principle 3.2)</h4> By using the Data Controller in combination with knowledge of data lineage (eg from SAS metadata or manual lookup table) it becomes possible to produce an automated report to identify exactly who - and hence which division - was involved in both the preparation and the validation of the all source data per reporting table for each reporting cycle. <h4>Processes should integrate and document key controls (principle 3.3)</h4> Data Controller can be used as a staging point for verifying the quality of data, eg when data from one department must be passed to another department for processing. The user access policy will be as per the existing policy for your SAS environment. <h2>Summary</h2> Whilst the circular provides valuable clarity on the expectations of the NBB, there are significant costs involved to prepare for, and maintain, compliance with the guidance. This is especially the case where reporting processes are disparate, and make use of disconnected EUCs and manual processes. The Data Controller for SAS® addresses and automates a number of pain points as specifically described in the circular. It is a robust and easy-to-use tool, actively maintained and <a href="http://docs.datacontroller.io">documented</a>, and provides an integrated solution on a tried and trusted platform for data management.
|
||||
<h4>Capture of Errors and Inconsistencies (principle 2.2)</h4> Data Controller has a number of features to ensure timely detection of Data Quality issues at source (such as cell validation, post edit hook scripts, duplicate removals, rejection of data with missing columns, etc etc). Where errors do make it into the system, a full history is kept (logs, copies of files etc) for all uploads and approvals. Emails of such errors can be configured for follow up. <h4>Tools and Techniques for Information Management Should be Automated (principle 2.3)</h4> The Data Controller can be configured to execute specific .sas programs after data validation. This enables the development of a secure and <em>integrated</em> workflow, and helps companies to avoid the additional documentation penalties associated with "miscellaneous unconnected computer applications" and manual information processing. <a href="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.53.38.png"><img class="aligncenter wp-image-963" src="/wp-content/uploads/2018/10/Screen-Shot-2018-10-13-at-22.53.38.png" alt="" width="278" height="128" /></a> <h4>Periodic Review & Improvements (principles 2.4 and 3.4)</h4> The Data Controller is actively maintained with the specific aim to reduce the cost of compliance with regulations such as NBB_2017_27. Our <a target="_blank" rel="noopener" href="https://slides.com/allanbowe/datacontroller/#/">roadmap</a> includes new features such as pre-canned reports, version 'signoff', and the ability to reinstate previous versions of data. <h4>A process for correction and final validation of reporting before submission (3.1)</h4> As a primary and dedicated tool for data corrections, Data Controller can be described once and used everywhere. <h4>List of Divisions Involved in Preparing Tables (principle 3.2)</h4> By using the Data Controller in combination with knowledge of data lineage (eg from SAS metadata or manual lookup table) it becomes possible to produce an automated report to identify exactly who - and hence which division - was involved in both the preparation and the validation of the all source data per reporting table for each reporting cycle. <h4>Processes should integrate and document key controls (principle 3.3)</h4> Data Controller can be used as a staging point for verifying the quality of data, eg when data from one department must be passed to another department for processing. The user access policy will be as per the existing policy for your SAS environment. <h2>Summary</h2> Whilst the circular provides valuable clarity on the expectations of the NBB, there are significant costs involved to prepare for, and maintain, compliance with the guidance. This is especially the case where reporting processes are disparate, and make use of disconnected EUCs and manual processes. The Data Controller for SAS® addresses and automates a number of pain points as specifically described in the circular. It is a robust and easy-to-use tool, actively maintained and <a target="_blank" rel="noopener" href="http://docs.datacontroller.io">documented</a>, and provides an integrated solution on a tried and trusted platform for data management.
|
||||
|
||||
@@ -13,4 +13,4 @@ tags:
|
||||
- Use Cases
|
||||
---
|
||||
|
||||
We caught up with <a href="https://www.linkedin.com/in/herbert-gro%C3%9Fmann-53690517a/">Herbert Grossmann</a> of DER Touristik to understand how Data Controller for SAS is used within the BICC and the types of challenges it solves. <a href="https://www.linkedin.com/in/herbert-gro%C3%9Fmann-53690517a/"><img class=" wp-image-1137 alignright" src="/wp-content/uploads/2020/08/0-1.jpeg" alt="" width="183" height="183" /></a> The previous article in this series can be found <a href="/data-controller-developer-perspective/">here</a>. <h2>Guten Tag, Herby! Can you tell us about your role within DER Touristik?</h2> Yes, I am working here as project manager for BI and Analytics and my department is the BICC (Business Intelligence Competence Centre), and we have an absolute focus on the SAS technology stack - so that’s my daily business. <h2>Great. And, I understand you guys are using Data Controller for SAS. What do you use it for?</h2> Well, mainly for managing control tables, that we have a lot of nowadays, in the data warehouse. But we also implemented what we call an "early bird booking system". There we have used the Approval process within Data Controller, which is excellent, because users, business departments etc, can approve data that would normally only be accessible within the back-end warehouse itself. So now they have an interface, which limits their access to specific views, and this is very useful - it was also highly commended by our management. <h2>So, business users can approve modifications to secure warehouse tables without having direct write-access themselves?</h2> Exactly <h2>Fantastic. Next question. How does having Data Controller make your life easier?</h2> Well - there is the version control of course, that gives us a much better traceability of changes to see what was changed by whom, at what time. And we have the immediate constraint checking which is also very useful because some of the tables are sensitive towards, let’s say, the changes of the primary key. And in the past when we did it the "old fashioned way" it was possible that by mistake that someone could cause duplicate primary keys or stuff like that, so this is now not possible anymore, which is very good. And like the example that I mentioned before, that now we can grant access to certain sensitive tables even for business users that would normally have no access, but we can decide whether to give them at least the right to <em>view</em> these tables, or during special events <em>edit</em> tables, or approve edits of those tables. So this gives a lot of opportunities, and makes it much easier than it was in the past. <h2>Nice! And so, talking about the past, before you had Data Controller, how did you manage modifications to data in SAS?</h2> We classically used two approaches - on one hand using SAS Enterprise Guide to directly edit tables or do imports, such as imports of excel sheets for example. On the other hand, we have some batch processes that also do imports of Excel tables or CSV tables. So those were the classic and standard ways. And of course especially the batch one we are still using for some files, depending on the situation. But we do no editing of tables directly with Enterprise Guide anymore because it is much safer and easier to use the Data Controller. <h2>Understood. So on the Data Controller side, what would you say were your favourite features and why?</h2> I would say that I like the editor as a whole very much. I think that is great that in the moment you make a table editable, you can define the ways in which you would edit the tables. Like whether there is some historic logging or not, and the fact you can set the constraints. And in the editor then you have a lot of Data Quality opportunities such as defining drop-down lists for certain attributes, which really makes editing the tables easier and much more comfortable. It was a little bit of a pain in the past but now it’s almost fun. <h2>That's great feedback! Is there anything else, any comments you would like to add?</h2> Yes, I like the fact that Data Controller is really just a part of the SAS environment. It’s not a completely separate application that you have to install somewhere, but a kind of pluggable part of the SAS environment. I liked it very much because then you still have everything in your hands. I mean I am not a developer but my knowledge of SAS is already enough to match the criteria to be able to handle the Data Controller as whole, to even do the updates and/or to modify things. And also it’s easy to show others who have experience with SAS how the tool works and what is to be done when there are data issues. And yeah, I think that’s a big advantage. <img class="wp-image-1140 aligncenter" src="/wp-content/uploads/2020/08/Group-1dt-1-e1597092362693.png" alt="SAS DER Touristik" width="242" height="213" />
|
||||
We caught up with <a target="_blank" rel="noopener" href="https://www.linkedin.com/in/herbert-gro%C3%9Fmann-53690517a/">Herbert Grossmann</a> of DER Touristik to understand how Data Controller for SAS is used within the BICC and the types of challenges it solves. <a target="_blank" rel="noopener" href="https://www.linkedin.com/in/herbert-gro%C3%9Fmann-53690517a/"><img class=" wp-image-1137 alignright" src="/wp-content/uploads/2020/08/0-1.jpeg" alt="" width="183" height="183" /></a> The previous article in this series can be found <a href="/data-controller-developer-perspective/">here</a>. <h2>Guten Tag, Herby! Can you tell us about your role within DER Touristik?</h2> Yes, I am working here as project manager for BI and Analytics and my department is the BICC (Business Intelligence Competence Centre), and we have an absolute focus on the SAS technology stack - so that’s my daily business. <h2>Great. And, I understand you guys are using Data Controller for SAS. What do you use it for?</h2> Well, mainly for managing control tables, that we have a lot of nowadays, in the data warehouse. But we also implemented what we call an "early bird booking system". There we have used the Approval process within Data Controller, which is excellent, because users, business departments etc, can approve data that would normally only be accessible within the back-end warehouse itself. So now they have an interface, which limits their access to specific views, and this is very useful - it was also highly commended by our management. <h2>So, business users can approve modifications to secure warehouse tables without having direct write-access themselves?</h2> Exactly <h2>Fantastic. Next question. How does having Data Controller make your life easier?</h2> Well - there is the version control of course, that gives us a much better traceability of changes to see what was changed by whom, at what time. And we have the immediate constraint checking which is also very useful because some of the tables are sensitive towards, let’s say, the changes of the primary key. And in the past when we did it the "old fashioned way" it was possible that by mistake that someone could cause duplicate primary keys or stuff like that, so this is now not possible anymore, which is very good. And like the example that I mentioned before, that now we can grant access to certain sensitive tables even for business users that would normally have no access, but we can decide whether to give them at least the right to <em>view</em> these tables, or during special events <em>edit</em> tables, or approve edits of those tables. So this gives a lot of opportunities, and makes it much easier than it was in the past. <h2>Nice! And so, talking about the past, before you had Data Controller, how did you manage modifications to data in SAS?</h2> We classically used two approaches - on one hand using SAS Enterprise Guide to directly edit tables or do imports, such as imports of excel sheets for example. On the other hand, we have some batch processes that also do imports of Excel tables or CSV tables. So those were the classic and standard ways. And of course especially the batch one we are still using for some files, depending on the situation. But we do no editing of tables directly with Enterprise Guide anymore because it is much safer and easier to use the Data Controller. <h2>Understood. So on the Data Controller side, what would you say were your favourite features and why?</h2> I would say that I like the editor as a whole very much. I think that is great that in the moment you make a table editable, you can define the ways in which you would edit the tables. Like whether there is some historic logging or not, and the fact you can set the constraints. And in the editor then you have a lot of Data Quality opportunities such as defining drop-down lists for certain attributes, which really makes editing the tables easier and much more comfortable. It was a little bit of a pain in the past but now it’s almost fun. <h2>That's great feedback! Is there anything else, any comments you would like to add?</h2> Yes, I like the fact that Data Controller is really just a part of the SAS environment. It’s not a completely separate application that you have to install somewhere, but a kind of pluggable part of the SAS environment. I liked it very much because then you still have everything in your hands. I mean I am not a developer but my knowledge of SAS is already enough to match the criteria to be able to handle the Data Controller as whole, to even do the updates and/or to modify things. And also it’s easy to show others who have experience with SAS how the tool works and what is to be done when there are data issues. And yeah, I think that’s a big advantage. <img class="wp-image-1140 aligncenter" src="/wp-content/uploads/2020/08/Group-1dt-1-e1597092362693.png" alt="SAS DER Touristik" width="242" height="213" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: EUC Management Systems need these 12 Attributes
|
||||
description: An EUC management system should automatically identify, clean, secure, backup, and integrate EUC data with full auditability, ownership, and approval.
|
||||
date: '2018-10-30 09:13:25'
|
||||
date: '2019-01-01 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './DC-UML-Activity-Diagram-2.png'
|
||||
@@ -16,14 +16,16 @@ tags:
|
||||
---
|
||||
|
||||
End User Computing (EUC) applications are unavoidable - the challenge is not to erase them, but to embrace automated approaches to EUC management that will identify, clean, secure, backup, and integrate EUC data with full auditability, ownership, and approval.
|
||||
|
||||
<h2>The Much-Maligned EUC</h2>
|
||||
EUC applications such as Excel, Access Databases, and locally executed programs, are often targeted as the source of a myriad of risks - such as financial misstatements, internal fraud, incorrect models, and potential for business process disruption. The rationale being that business developed / owned applications are not subject to the same access controls, development & testing standards, documentation and release management processes as can be found over the "IT Fence". Whilst this is probably true, the inherent flexibility of EUCs that can be quickly updated without service desk requests, project codes, or lost arms & legs - means that EUCs are, regardless, here to stay.
|
||||
|
||||
The challenge is to find a way to shine a light onto this "Shadow IT", and provide a framework by which EUC data can be extracted in a simple, safe, secure, scalable, and auditable fashion. <a href="/wp-content/uploads/2018/10/DC-UML-Use-Case-Diagram-EUC.png"><img class="aligncenter size-large wp-image-1008" src="/wp-content/uploads/2018/10/DC-UML-Use-Case-Diagram-EUC.png" alt="EUC Use Case Diagram" /></a>
|
||||
|
||||
<h2>EUCs can be Controlled</h2>
|
||||
The 'war on EUCs' cannot be won - it simply isn't practical to ban them, or to migrate / redevelop every closely held and highly complex legacy VBA application. Until alternative solutions for Citizen Developers to build Enterprise Apps (such as <a href="https://sasjs.io">SASjs</a>) become mainstream, simple measures / controls on the EUCs themselves must be implemented - such as version control, readonly attributes, embedded documentation, peer review etc. In the meantime, a management system for EUCs is the ideal place for capturing the requisite metadata needed to monitor, audit, and secure the data therein. Such a management system should have, as a minimum, the following attributes:
|
||||
The 'war on EUCs' cannot be won - it simply isn't practical to ban them, or to migrate / redevelop every closely held and highly complex legacy VBA application. Until alternative solutions for Citizen Developers to build Enterprise Apps (such as <a target="_blank" rel="noopener" href="https://sasjs.io">SASjs</a>) become mainstream, simple measures / controls on the EUCs themselves must be implemented - such as version control, readonly attributes, embedded documentation, peer review etc. In the meantime, a management system for EUCs is the ideal place for capturing the requisite metadata needed to monitor, audit, and secure the data therein. Such a management system should have, as a minimum, the following attributes:
|
||||
<h3>EUC Data Quality at Source</h3>
|
||||
The ability to run data quality routines at the point of data upload (from EUC to secure IT environment) provides instant feedback to EUC operators that will allow them to make corrections and avoid costly post-upload investigations, re-runs, or worse - incorrect results. As part of this process, it should be easy to create and update those Data Quality rules. A longer discussion of Data Quality can be found <a href="https://www.linkedin.com/pulse/zen-art-data-quality-allan-bowe/">here</a>.
|
||||
The ability to run data quality routines at the point of data upload (from EUC to secure IT environment) provides instant feedback to EUC operators that will allow them to make corrections and avoid costly post-upload investigations, re-runs, or worse - incorrect results. As part of this process, it should be easy to create and update those Data Quality rules. A longer discussion of Data Quality can be found <a target="_blank" rel="noopener" href="https://www.linkedin.com/pulse/zen-art-data-quality-allan-bowe/">here</a>.
|
||||
<h3>EUC Data Review (4 eyes)</h3>
|
||||
After EUC data is submitted, it should be reviewed before the target database is updated. It should be possible (but not mandatory) for this check to be performed by a different individual. When performing that check, it should only be necessary to review new / changed / deleted records. For changed records, the reviewer should also be able to see the original values. If the data is approved, the target table is updated. If rejected, the staged data can simply be archived.
|
||||
<h3>Roles & Responsibilities (RACI)</h3>
|
||||
|
||||
@@ -23,9 +23,9 @@ There are many aspects to full Sarbanes-Oxley (SOX) compliance, the [legislation
|
||||
|
||||
Data Controller facilitates internal controls through a 4 eyes review & approve mechanism for data changes. This, combined with data validation and an integrated workflow feature, provides a mechanism to easily track and report on the number of internal controls (quality rules, signoffs, rejections), as well as the frequency they are applied, who is applying them, which data items the controls relate to, and who is performing them. Such metrics can be compared and contrasted with pre-existing and current quality measures to help determine control effectiveness. Variations in the number of submit / approve cycles between reporting teams, also provide objective and repeatable measurements to support the assessment of the effectiveness of internal controls.
|
||||
|
||||
<div class="imgHolder"><a href="https://www.govinfo.gov/content/pkg/BILLS-107hr3763enr/pdf/BILLS-107hr3763enr.pdf"><img class="wp-image-1105 size-full aligncenter" title="Sec 404. (Sarbanes-Oxley)" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-07-17-57-01.png" alt="Sarbanes Oxley"/></a><caption>Sarbanes Oxley</caption></div>
|
||||
<div class="imgHolder"><a target="_blank" rel="noopener" href="https://www.govinfo.gov/content/pkg/BILLS-107hr3763enr/pdf/BILLS-107hr3763enr.pdf"><img class="wp-image-1105 size-full aligncenter" title="Sec 404. (Sarbanes-Oxley)" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-07-17-57-01.png" alt="Sarbanes Oxley"/></a><caption>Sarbanes Oxley</caption></div>
|
||||
|
||||
Section 404 is widely considered the most onerous part of Sarbanes-Oxley, as the documentation and testing of all the controls requires significant time and effort. To address this, the <a href="https://pcaobus.org/">Public Company Accounting Oversight Board</a> (PCAOB - a US non-profit created by the Sarbanes-Oxley act itself) released<a href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"> additional guidance</a> to assist management and auditors in producing their reports. This is officially labeled "Auditing Standard No. 5 - <em>An Audit of Internal Control Over Financial Reporting That Is Integrated with An Audit of Financial Statements"</em> A few points are highlighted by the guidance in this standard that are pertinent to users of Data Controller. <h2>PCAOB AS5 Sec24 - Controls Over Management Override</h2> Management Overrides (the freedom to simply "replace" reporting figures based on, presumably, sound judgement) are entity level controls that can be easily captured (in a centralised manner) by Data Controller. This in fact, is the "core functionality" of the tool. Data Stewards / Data Processors (Editors) make the change, then one or more Data Owners / Data Controllers (Approvers) sign it off before it is applied to the target table. A copy of the original excel file (if used) and a record of who made the change, when, what the change was, and why (if a reason is provided) is recorded. <a href="https://docs.datacontroller.io/dcc-validations/">Data Validation</a> rules can also be defined to ensure that inputs fit the desired pattern(s). <a href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"><img class="aligncenter wp-image-1122" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-10-10-41-12.png" alt="Sarbanes Oxley sas management overrides" width="887" height="409" /></a> For fun, we made a short video for this part:
|
||||
Section 404 is widely considered the most onerous part of Sarbanes-Oxley, as the documentation and testing of all the controls requires significant time and effort. To address this, the <a target="_blank" rel="noopener" href="https://pcaobus.org/">Public Company Accounting Oversight Board</a> (PCAOB - a US non-profit created by the Sarbanes-Oxley act itself) released<a target="_blank" rel="noopener" href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"> additional guidance</a> to assist management and auditors in producing their reports. This is officially labeled "Auditing Standard No. 5 - <em>An Audit of Internal Control Over Financial Reporting That Is Integrated with An Audit of Financial Statements"</em> A few points are highlighted by the guidance in this standard that are pertinent to users of Data Controller. <h2>PCAOB AS5 Sec24 - Controls Over Management Override</h2> Management Overrides (the freedom to simply "replace" reporting figures based on, presumably, sound judgement) are entity level controls that can be easily captured (in a centralised manner) by Data Controller. This in fact, is the "core functionality" of the tool. Data Stewards / Data Processors (Editors) make the change, then one or more Data Owners / Data Controllers (Approvers) sign it off before it is applied to the target table. A copy of the original excel file (if used) and a record of who made the change, when, what the change was, and why (if a reason is provided) is recorded. <a target="_blank" rel="noopener" href="https://docs.datacontroller.io/dcc-validations/">Data Validation</a> rules can also be defined to ensure that inputs fit the desired pattern(s). <a target="_blank" rel="noopener" href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"><img class="aligncenter wp-image-1122" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-10-10-41-12.png" alt="Sarbanes Oxley sas management overrides" width="887" height="409" /></a> For fun, we made a short video for this part:
|
||||
|
||||
`youtube: https://youtu.be/iY3KQZL4ok0`
|
||||
|
||||
@@ -39,7 +39,7 @@ Below is an example of column level lineage. Like Table Level lineage, this can
|
||||
|
||||
The ability to define additional data lineages, outside of SAS (eg between spreadsheets or other reporting systems) is in the product roadmap, along with lineage from SAS Viya. <h2>PCAOB AS5 App B - Benchmarking of Automated Controls</h2> The use of IT secured financial controls can significantly reduce the cost of Sarbanes-Oxley compliance testing following the first year assessment, particularly where the source code is secured and cannot be modified by users. The core programs (services) within the Data Controller application that perform data signoffs are mature, distinct and change tracked - so it is possible for Data Controller to be upgraded in-place without affecting the benchmarking strategy. This contrasts with spreadsheet based control mechanisms, which must be revalidated in each reporting period.
|
||||
|
||||
<div class="imgHolder"><a href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"><img class="aligncenter" title="PCAOB Release 2007-005A, Appendix B" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-08-22-15-50.png" alt="Sarbanes Oxley SAS"/></a><caption>PCAOB Release 2007-005A, Appendix B</caption></div>
|
||||
<div class="imgHolder"><a target="_blank" rel="noopener" href="https://pcaobus.org/Rulemaking/Docket%20021/2007-06-12_Release_No_2007-005A.pdf"><img class="aligncenter" title="PCAOB Release 2007-005A, Appendix B" src="/wp-content/uploads/2020/08/Screenshot-from-2020-08-08-22-15-50.png" alt="Sarbanes Oxley SAS"/></a><caption>PCAOB Release 2007-005A, Appendix B</caption></div>
|
||||
|
||||
## Sarbanes-Oxley Act Section 1102 - Tampering
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ tags:
|
||||
- VBA
|
||||
---
|
||||
|
||||
Data Controller was implemented at Siemens Healthineers to facilitate their SAS-Powered Smart Data Catalog and integrate with Data Lineage reporting. We are grateful to <a href="https://www.linkedin.com/in/helen-stark-5bb15b6a/">Helen Stark</a> (Power User) and <a href="https://www.linkedin.com/in/hans-juergen-kopperger-942634b7/">Hans-Juergen Kopperger</a> (SAS App Developer) for sharing their "before and after" experiences. The previous article in this series is available <a href="/der-touristik/">here</a>.
|
||||
Data Controller was implemented at Siemens Healthineers to facilitate their SAS-Powered Smart Data Catalog and integrate with Data Lineage reporting. We are grateful to <a target="_blank" rel="noopener" href="https://www.linkedin.com/in/helen-stark-5bb15b6a/">Helen Stark</a> (Power User) and <a target="_blank" rel="noopener" href="https://www.linkedin.com/in/hans-juergen-kopperger-942634b7/">Hans-Juergen Kopperger</a> (SAS App Developer) for sharing their "before and after" experiences. The previous article in this series is available <a href="/der-touristik/">here</a>.
|
||||
|
||||
---
|
||||
|
||||
@@ -76,7 +76,7 @@ In the past we had a custom Stored Process web app for uploading excel files, ba
|
||||
|
||||
I would often receive support tickets in relation to this upload, the cause of which was often due to the diversity of our excel templates, and being unsure which was the right template...So, we would have a lot of discussions about how to bring data into the backend in a controlled manner.
|
||||
|
||||
Then one day, I got information through <a href="https://sasusergroup.de/">SAS User Group Germany</a> that you provide a solution with Data Controller. I was initially interested in the <a href="https://docs.datacontroller.io/videos/#data-lineage">Data Lineage</a> functionality, but then I understood the main concept behind Data Controller. And for me the main benefit is that I can save a lot of time - with out of the box features like the web data editor, and the web upload facility with excel spreadsheet drag and drop. And there is the automatic workflow behind with the mandatory approval step. Since we implemented Data Controller, we no longer get those support tickets.
|
||||
Then one day, I got information through <a target="_blank" rel="noopener" href="https://sasusergroup.de/">SAS User Group Germany</a> that you provide a solution with Data Controller. I was initially interested in the <a target="_blank" rel="noopener" href="https://docs.datacontroller.io/videos/#data-lineage">Data Lineage</a> functionality, but then I understood the main concept behind Data Controller. And for me the main benefit is that I can save a lot of time - with out of the box features like the web data editor, and the web upload facility with excel spreadsheet drag and drop. And there is the automatic workflow behind with the mandatory approval step. Since we implemented Data Controller, we no longer get those support tickets.
|
||||
|
||||
### Fantastic. If you had to pick your top features, what would they be?
|
||||
|
||||
@@ -84,7 +84,7 @@ The main benefit is getting data controlled, and into the backend. The controlle
|
||||
|
||||
The transparency of the history page is another benefit. I can look at every requested submit or approval - what changes have been applied, what changes have been submitted, and what changes have been approved. This helps us a lot to get data transparency.
|
||||
|
||||
The <a href="https://docs.datacontroller.io/emails/">email alerts</a> is a great feature. For the communication of changes, we had previously created a team's collaboration chat. e.g. if someone did a change and needed to request an approval. But with email alerts, the notification of changes is now automatically sent to the responsible data owner, who can immediately click the email link and do his approval. This speeds up the whole process.
|
||||
The <a target="_blank" rel="noopener" href="https://docs.datacontroller.io/emails/">email alerts</a> is a great feature. For the communication of changes, we had previously created a team's collaboration chat. e.g. if someone did a change and needed to request an approval. But with email alerts, the notification of changes is now automatically sent to the responsible data owner, who can immediately click the email link and do his approval. This speeds up the whole process.
|
||||
|
||||
Another advantage is the "database approach" for updates. So, someone is changing one row in a table which is connected to his use case, another guy can change other rows of the same table, nearly simultaneously. Because not everyone is changing the same rows. Everyone has their own subset of rows, their own "workspace" within one table. In the past we would have one excel template, and this would always override all values. We would have a lot of excel templates going around our colleagues, so there were always conflicts of overrides and versioning, and stuff like that. With Data Controller, it's now a simple, easy and transparent data capture process.
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "Data Controller now supports Snowflake"
|
||||
description: Data Controller now supports Snowflake as a data modification target, enabling governed editing, approval workflows, and full audit trails for Snowflake tables on both SAS 9 and SAS Viya.
|
||||
date: '2026-03-14 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './snowflake.png'
|
||||
tags:
|
||||
- Data Management
|
||||
- Releases
|
||||
- Snowflake
|
||||
---
|
||||
|
||||
We're delighted to announce that Data Controller now supports **Snowflake** as a data modification target! This means you can use Data Controller's full suite of governed data editing, approval workflows, and audit trail capabilities directly against your Snowflake tables — on both SAS 9 and SAS Viya.
|
||||
|
||||
## What This Means for You
|
||||
|
||||
If your organisation stores data in [Snowflake](https://www.linkedin.com/company/snowflake-computing/), you can now:
|
||||
|
||||
- **Edit** Snowflake table data through the familiar Data Controller interface
|
||||
- **Submit** changes for review and approval before they are applied
|
||||
- **Audit** every change with a complete, tamper-evident history
|
||||
|
||||
Say goodbye to uncontrolled spreadsheet uploads and ad-hoc database edits. Every Snowflake modification now flows through the same robust approval process that governs the rest of your Data Controller tables.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
On SAS Viya, the library should be assigned in the autoexec. On SAS 9, Snowflake library assignments can come from the autoexec, or from **SAS Metadata**. Simply register your Snowflake library in SMC or SAS DI Studio as you normally would, and Data Controller will pick it up automatically.
|
||||
|
||||
Both the `SNOW` (alias) and `SASIOSNF` engines are supported.
|
||||
|
||||
|
||||
## Native Passthrough SQL
|
||||
|
||||
Under the hood, Data Controller uses **native passthrough SQL** to interact with Snowflake. This means:
|
||||
|
||||
- Modifications are pushed directly to Snowflake using its own SQL dialect
|
||||
- The target table does **not** need to be indexed (but must unique on its primary key)
|
||||
- Maximum performance and compatibility with Snowflake's architecture is assured
|
||||
|
||||
## Getting Started
|
||||
|
||||
Snowflake support is available out of the box — no additional configuration is required beyond your existing Snowflake library assignment in SAS. More details on Data Controller are available in the [documentation](https://docs.datacontroller.io).
|
||||
|
||||
Ready to bring governed data modification to your Snowflake environment? We'd love to [hear from you](/contact)!
|
||||
|
After Width: | Height: | Size: 30 KiB |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "v6.0 Release: Viya API Explorer"
|
||||
title: 'v6.0 Release: Viya API Explorer'
|
||||
description: Data Controller community tier now includes an API explorer! We've also overhauled the (in)format ingestion capability, and revamped our pricing (now with unlimited users across all tiers).
|
||||
date: '2023-06-26 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
@@ -21,7 +21,6 @@ This lets you easily trigger the (GET) APIs and explore the responses without ha
|
||||
|
||||
<iframe title="Browsing Viya API in Data Controller" width="560" height="315" src="https://vid.4gl.io/videos/embed/e284f815-a6dc-4998-80bd-152d54cb81a9?title=0" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>
|
||||
|
||||
|
||||
Here we grab the raw JSON for pasting into VS Code:
|
||||
|
||||
<iframe title="Grabbing JSON from Viya APIs with Data Controller" width="560" height="315" src="https://vid.4gl.io/videos/embed/18914633-342b-48f1-9021-bb01a8b33198?title=0&warningTitle=0" frameborder="0" allowfullscreen="" sandbox="allow-same-origin allow-scripts allow-popups"></iframe>
|
||||
@@ -32,23 +31,21 @@ And here we toggle the start / limit parameters to bring back more values:
|
||||
|
||||
We would love YOUR feedback as to how we can extend this API explorer to make it an even more useful tool!
|
||||
|
||||
|
||||
|
||||
## Unlimited Users
|
||||
|
||||
If you've been following us for a while you've probably heard the '5 users free' tagline. Well - you will hear it no more, as we now offer **unlimited users for all tier levels**!
|
||||
|
||||
That's right, you can download Data Controller (Community Edition) and use it across your entire enterprise TODAY, without spending a penny.
|
||||
|
||||
If, however, you would like priority support and full access to all features, we ask that you engage us on <a href="https://datacontroller.io/pricing">paid subscription plan</a>.
|
||||
If, however, you would like priority support and full access to all features, we ask that you engage us on <a target="_blank" rel="noopener" href="https://datacontroller.io/pricing">paid subscription plan</a>.
|
||||
|
||||
## (IN)FORMAT Capabilities
|
||||
|
||||
Previously we only supported ingestion of run-of-the-mill SAS formats. Following customer feedback, we have now expanded this capability to include:
|
||||
|
||||
* Informats
|
||||
* Multilabel Formats
|
||||
* NotSorted Formats
|
||||
- Informats
|
||||
- Multilabel Formats
|
||||
- NotSorted Formats
|
||||
|
||||
The addition of these format types broke the data model we were using previously for holding format data. We had incorrectly assumed that the CNTLOUT dataset could be keyed on TYPE, FMTNAME and START.
|
||||
|
||||
@@ -68,7 +65,6 @@ We've added a new screen (under the username dropdown) to show system details as
|
||||
|
||||
This screen is also available for regular users (those not in the Data Controller admin group), just without the additional buttons.
|
||||
|
||||
|
||||
## Load More Values
|
||||
|
||||
We've added the ability to 'load more' history on the history page, as well as the ability to [show more history by default](https://docs.datacontroller.io/dcc-options/#history_rows)
|
||||
@@ -81,31 +77,14 @@ We've added the ability to 'load more' history on the history page, as well as t
|
||||
|
||||
Some of the issues we've zapped:
|
||||
|
||||
* Enable data-catalog refresh of a single library when invalid libraries are present
|
||||
* Prevent error when attempting an UNLOCK of an already-unlocked table
|
||||
* Show Viya avatar when web app is served from a different domain
|
||||
* Bug with delete-only uploads not appearing in the audit table
|
||||
* Show special missing values on VIEW screen
|
||||
- Enable data-catalog refresh of a single library when invalid libraries are present
|
||||
- Prevent error when attempting an UNLOCK of an already-unlocked table
|
||||
- Show Viya avatar when web app is served from a different domain
|
||||
- Bug with delete-only uploads not appearing in the audit table
|
||||
- Show special missing values on VIEW screen
|
||||
|
||||
## Roadmap
|
||||
|
||||
Looking to the future, we are actively tidying up the codebase to publish it as 'source-available' (the source is already available to existing customers). We are also investigating the HandsOnTable "Formula" feature to see if we can implement it on the EDIT grid.
|
||||
|
||||
If you would like to see any new features in DC, or would like to kick the tyres and give it a whirl, do [get in touch](https://datacontroller.io/contact)!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: 'Complex Excel Uploads with Excel Maps'
|
||||
description: Extract scattered, dynamic data from any Excel workbook using configurable Excel Maps - no manual reformatting required.
|
||||
date: '2026-07-26 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './dc-capture-review-approve.jpeg'
|
||||
---
|
||||
|
||||
# Complex Excel Uploads with Excel Maps
|
||||
|
||||
Not all spreadsheets arrive in a tidy, tabular format - and now they don't have to. Data Controller's **Complex Excel Upload** feature lets you extract data from anywhere in a workbook using an **Excel Map**: a configurable set of rules for loading specific cells and ranges. Each rule defines a range ID, a start, and an end, and can use absolute or relative references - for example, matching a specific string in a cell, then moving down and across to capture the desired value. This makes it ideal for business reports and financial spreadsheets where the layout is dynamic and the data you need is scattered across multiple locations.
|
||||
|
||||
Once you've dragged in the spreadsheet and the rules have run, submitting the data stages it alongside a copy of the original Excel file - preserving a complete audit trail right back to the source. The extracted data is transposed into a single column in the [`MPE_XLMAP_DATA`](https://docs.datacontroller.io/tables/mpe_xlmap_data/) table, ready for you to build jobs that present it however your business requires. You can also adjust your map's attributes in the [`MPE_XLMAP_INFO`](https://docs.datacontroller.io/tables/mpe_xlmap_info/) table to load data into different target tables.
|
||||
|
||||
All of this flows through the standard Data Controller approval process and data quality checks - governance included, straight out of the box.
|
||||
|
||||
See it in action:
|
||||
|
||||
<div style="position: relative; padding-top: 56.25%; margin-bottom: 2rem;"><iframe title="Complex Excel Uploads" width="100%" height="100%" src="https://vid.4gl.io/videos/embed/7jRSxNs2ENJ3dBEZun4cPf?peertubeLink=0" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>
|
||||
|
||||

|
||||
|
||||
Learn more in the [documentation](https://docs.datacontroller.io/excel/#complex-excel-uploads).
|
||||
|
After Width: | Height: | Size: 254 KiB |
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: 'Every SAS® Project Needs to Load Data from Excel'
|
||||
description: After years of SAS consulting, one pattern emerged - every project needed Excel data loads, and every project built them from scratch. Here is how that journey led to Data Controller.
|
||||
date: '2026-08-17 19:15:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './sasprojects.jpeg'
|
||||
---
|
||||
|
||||
After several years of SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
|
||||
|
||||
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
|
||||
|
||||
This process could literally take weeks.
|
||||
|
||||
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
|
||||
|
||||
It was a godsend.
|
||||
|
||||
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
|
||||
|
||||
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had SAS Viya compatibility and a growing number of satisfied (paying) customers.
|
||||
|
||||
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
|
||||
|
||||
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
|
||||
|
||||
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
|
||||
|
||||
The need to load ad-hoc data into SAS isn't going away.
|
||||
|
||||
Our mission is to make that process simple, secure, and fast.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/) - or start today with the free [Community version](https://datacontroller.io/pricing/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
After several years #SAS consulting I noticed EVERY SINGLE PROJECT had a need to load data from Excel.
|
||||
|
||||
And EVERY SINGLE PROJECT would require dedicated SAS code (or ETL jobs) to read that data, cleanse, append to target, and to run in a secure production environment under batch credentials. This meant a dev/test/accept(+signoff)/prod lifecycle.
|
||||
|
||||
This process could literally take weeks.
|
||||
|
||||
As projects went by, I crafted a generic data capture tool using VBA, Stored Processes, and a shared network drive to share CSVs with SAS. It was a ZERO CODE solution that worked really well, and allowed ANY table to be configured so that business users could make changes in Excel and upload directly to production (with approval step).
|
||||
|
||||
It was a godsend.
|
||||
|
||||
Still, it had major drawbacks - users had to self-pull the latest version, and we still had this insecure network drive situation.
|
||||
|
||||
By 2020 the codebase was completely re-written as a SASjs Powered Web Application. The shared network drive was gone. The browser always has the latest version. We had #SASViya compatibility and a growing number of satisfied (paying) customers.
|
||||
|
||||
By 2022 we had advanced Data Quality features, support for all flavours of SAS (Base, Meta, Viya), special SAS missing numerics and the ability to update formats in catalogs. Big-name customers in Banking, Insurance, Government, Travel, Pharma and other sectors.
|
||||
|
||||
In 2024 we had support for complex excel uploads (extract cells / ranges from anywhere in a workbook), detailed data audit, and a data rollback feature.
|
||||
|
||||
In 2026 we have a streamlined and tightened Viya deploy and many more validations - such as regex, and soon, frontend formulae.
|
||||
|
||||
The need to load ad-hoc data into SAS isn't going away.
|
||||
|
||||
Our mission is to make that process simple, secure, and fast.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a Discovery Call (link in comments) - or start today with the free Community version.
|
||||
|
||||
#datamanagement
|
||||
|
||||
Image prompt: A flat-style illustration showing an Excel spreadsheet with an arrow flowing into a secure database server, with a subtle timeline (VBA era to modern web app) in the background, blue and green corporate palette, clean minimal design suitable for a LinkedIn/blog cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 582 KiB |
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: 'FSEDIT-Style Paging in the Edit Record Form'
|
||||
description: Page through your data one record at a time in the Edit Record dialog - a modern take on the classic SAS PROC FSEDIT full-screen editor.
|
||||
date: '2026-08-09 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './example.png'
|
||||
---
|
||||
|
||||
# FSEDIT-Style Paging in the Edit Record Form
|
||||
|
||||
Anyone who has worked with SAS for a while will remember `PROC FSEDIT`. Part of the SAS/FSP (Full-Screen Product) family alongside `PROC FSVIEW` and `PROC FSBROWSE`, it was for years the standard way to edit a dataset interactively: one observation per screen, typing directly over values, and commiting straight to the underlying dataset. That focused, one-record-at-a-time view was a simple way to amend data without getting lost in a wide grid - though edits landed immediately, with no staging or approval step in between.
|
||||
|
||||
Data Controller's **Edit Record** dialog brings that experience into the browser. Instead of scanning across dozens of columns in the grid, you open a single row as a form - every field labelled and stacked vertically, with dropdowns, validations and formatting applied just as they are in the main editor.
|
||||
|
||||
You can **page through your records** without leaving the form. Previous and Next arrows move you one record at a time, and a **Current row** indicator shows exactly where you are in the extract. It's the fastest way to work through a set of rows methodically - reviewing, correcting and moving on - especially for wide tables where horizontal scrolling in the grid slows you down.
|
||||
|
||||
<iframe title="Forwards_Backwards_in_Edit_Record_Modal" width="560" height="315" src="https://vid.4gl.io/videos/embed/o8yzHqRqTWhYo6cgNACRkg" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
As always, everything flows through the standard Data Controller workflow: your changes are staged, validated and sent for approval before they reach the target table, with a full audit trail retained.
|
||||
|
||||
Administrators who prefer to keep the grid-only experience can disable the dialog with the [`DC_RESTRICT_EDITRECORD`](https://docs.datacontroller.io/dcc-options/#dc_restrict_editrecord) option.
|
||||
|
After Width: | Height: | Size: 436 KiB |
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: 'Some of You Will "Git" This'
|
||||
description: Pull requests, diffs, hooks, tests and pipelines - for your production data. Data Controller brings the git workflow to master data management in SAS.
|
||||
date: '2026-08-13 14:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './gitflow.png'
|
||||
---
|
||||
|
||||
# Some of You Will "Git" This
|
||||
|
||||
If you could make a "pull request" for production data, how might that look?
|
||||
|
||||
Perhaps:
|
||||
|
||||
- ✅ A [before / after comparison](https://docs.datacontroller.io/dc-userguide/) of the values that had changed
|
||||
- ✅ [Security](https://docs.datacontroller.io/dcc-security/) (who can submit / approve in each "repo")
|
||||
- ✅ Only store the changed data (not full copies)
|
||||
- ✅ A new "diff" when the base table changes
|
||||
- ✅ Ability to choose [one or more approvers](https://docs.datacontroller.io/dcc-tables/)
|
||||
- ✅ [HOOK scripts](https://docs.datacontroller.io/dcc-tables/) before / after a commit
|
||||
- ✅ Tests ([DQ checks](https://docs.datacontroller.io/dcc-validations/)) prior to merge
|
||||
- ✅ A [pipeline](https://docs.datacontroller.io/dcc-tables/) following a merge
|
||||
- ✅ [Emails](https://docs.datacontroller.io/emails/) for "status" changes
|
||||
- ✅ Comments by reviewers
|
||||
- ✅ Full [history](https://docs.datacontroller.io/dc-userguide/) of changes
|
||||
|
||||
Sound familiar? This is exactly how developers have managed *code* for decades - and Data Controller brings the same discipline to *data*. Every edit to a table in SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded. The audit trail captures who changed what, when and why - the `git blame` of master data.
|
||||
|
||||
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of [Data Controller for SAS®](https://datacontroller.io) - which has ALL these features - and "push" to your "main" environment for some "master" data management!
|
||||
|
||||
<!-- Source LinkedIn post:
|
||||
Some of you will "git" this..
|
||||
|
||||
If you could make a "pull request" for production data, how might that look?
|
||||
|
||||
Perhaps:
|
||||
|
||||
✅ A before / after comparison of the values that had changed
|
||||
✅ Security (who can submit / approve in each "repo")
|
||||
✅ Only store the changed data (not full copies)
|
||||
✅ A new "diff" when the base table changes
|
||||
✅ Ability to choose one or more approvers
|
||||
✅ HOOK scripts before / after a commit
|
||||
✅ Tests (DQ checks) prior to merge
|
||||
✅ A pipeline following a merge
|
||||
✅ Emails for "status" changes
|
||||
✅ Comments by reviewers
|
||||
✅ Full history of changes
|
||||
|
||||
Sound familiar? This is exactly how developers have managed CODE for decades - and Data Controller brings the same discipline to DATA.
|
||||
|
||||
Every edit to a table in Data Controller for SAS® goes through an edit - stage - approve workflow: changes are staged as a diff, checked by validation rules and hook scripts, reviewed and approved by the people you choose, then loaded.
|
||||
|
||||
The audit trail captures who changed what, when and why - the "git blame" of master data.
|
||||
|
||||
If you're ready to "commit" to Data Control, we invite you to "clone" the latest version of Data Controller for SAS - which has ALL these features - and "push" to your "main" environment for some "master" data management!
|
||||
|
||||
Link in the comments below 👇
|
||||
|
||||
#dataops #mdm #masterdata #dataquality #sas #sasjs #datagovernance
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A clean, flat-design illustration in the Data Controller brand style showing a database table flowing through a git-style pull-request workflow: rows of a data table on the left passing through a "stage" step shown as a highlighted before/after diff of changed cells, then an approval checkpoint with a tick and reviewer avatars, then merging into a production database cylinder on the right. Git-flavoured touches: a branch line diagram arcing from "staging" branch into "main", small commit dots along the line. Dark navy background, teal/green accents, orange highlights on the changed cells. Minimal text: "stage -> approve -> merge". 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 512 KiB |
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: 'Horror Stories of Business Data Ingestion'
|
||||
description: Real-world tales of business data ingestion gone wrong - from broken ETL jobs to network-share "audit trails" and the Access database under the operations desk.
|
||||
date: '2026-08-15 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
previewImg: './horror.jpeg'
|
||||
tags:
|
||||
- Master Data Management
|
||||
---
|
||||
|
||||
# Horror Stories of Business Data Ingestion
|
||||
|
||||
> "We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
|
||||
|
||||
> "I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
|
||||
|
||||
> "We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
|
||||
|
||||
> "Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
|
||||
|
||||
> "We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
|
||||
|
||||
> "If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
|
||||
|
||||
> "We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
|
||||
|
||||
> "We don't have any business data ingestion, we only load source data, and it is never modified"
|
||||
|
||||
> "We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - [Xavier Bizoux](https://www.linkedin.com/in/xavierbizoux) ([source](https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29))
|
||||
|
||||
Coupled with:
|
||||
|
||||
> "We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
|
||||
|
||||
What are YOUR horror stories?
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Horror Stories of Business Data Ingestion
|
||||
|
||||
"We spent 6 weeks recruiting a Data Engineer, who built several complex ETL workflows, which promptly broke on his departure thanks to a minor change in the source file"
|
||||
|
||||
"I wrote the SQL update script myself, which was working fine until my holiday cover adjusted the where clause, and wiped half the records. At least, that's what I think happened. We didn't see the discrepancy until the following month."
|
||||
|
||||
"We often need to upload new mapping files and so all the Business Analysts in the Customer Insights team are being trained on the enterprise ETL tool"
|
||||
|
||||
"Our DBA does the manual updates, but he only has 0.5 free days per month allocated to the task - and he will only do it AFTER month end"
|
||||
|
||||
"We have a dedicated data engineering team and they can knock out an ETL job in a few minutes. The problem is that it takes 4-8 hours of documentation and meetings to set the requirements, and 4-8 weeks to deploy the job into production"
|
||||
|
||||
"If you need to change the data values, just double click the table in our .NET application and type it in. Remember to close it afterwards though, else the overnight batch job will fail due to a physical lock. This happens quite a lot - it's safest to just close the application afterwards and re-open it."
|
||||
|
||||
"We keep an audit trail of ALL manual updates. Whoever makes the change will just open the spreadsheet, fill in the necessary records, and save the file with a new version number on the department network share"
|
||||
|
||||
"We don't have any business data ingestion, we only load source data, and it is never modified"
|
||||
|
||||
"We export the data from the web report into Excel. We copy/paste the data into another Excel sheet where we execute some VBscript macros that were developed by a guy who left the company. After that, the updated data are saved as a csv file on a network drive. The file is then picked up by a batch job and loaded into the database. The job is scheduled to run every 15 minutes. The updated records can then be displayed in our reports." - Xavier Bizoux (https://www.linkedin.com/in/xavierbizoux)
|
||||
Source comment: https://www.linkedin.com/feed/update/urn:li:activity:7183406509450547200?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7183406509450547200%2C7184815374624772096%29&dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287184815374624772096%2Curn%3Ali%3Aactivity%3A7183406509450547200%29
|
||||
|
||||
Coupled with:
|
||||
|
||||
"We don't load our data to the DataWarehouse, it's too difficult. We just stick it all in the team ACCESS database on the PC under the operations desk"
|
||||
|
||||
What are YOUR horror stories?
|
||||
|
||||
#masterdatamanagement #dataops #datagovernance
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A spooky-but-professional flat-design illustration in the Data Controller brand style for a "Horror Stories of Business Data Ingestion" post: a haunted-house style scene rendered in dark navy, with ghostly spreadsheets, a broken ETL pipeline (cracked pipe segments leaking data rows), an old Microsoft Access database icon lurking under a desk, and a graveyard of tombstones labelled with file versions like "mapping_v12_FINAL_v2.xlsx". A full moon and a few bats in the background. Teal/green and orange accents for the eerie glow on the data elements. Humorous, Halloween-flavoured but still suitable for a B2B data product site. Minimal text. 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: 'SAS-isms: Library Info'
|
||||
description: Expanding a VIEW library in Data Controller surfaces engine, size, paths, table count and schema - with the same data queryable from the underlying tables.
|
||||
date: '2026-08-11 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- SAS-isms
|
||||
previewImg: './library-info.jpeg'
|
||||
---
|
||||
|
||||
# Library Info
|
||||
|
||||
One of many "SAS-isms" in Data Controller for SAS®.
|
||||
|
||||
When expanding a VIEW library, Data Catalog items are surfaced:
|
||||
|
||||
- Library Engine
|
||||
- Library Size (Bytes, MB, GB)
|
||||
- Paths (if BASE engine)
|
||||
- Table Count
|
||||
- Schema (if a database)
|
||||
|
||||

|
||||
|
||||
And more. Much of this data can also be queried from the underlying tables:
|
||||
|
||||
- [MPE_DATACATALOG_LIBS](https://docs.datacontroller.io/tables/mpe_datacatalog_libs) - engine, paths, permissions, owners, schemas, metadata name / id
|
||||
- [MPE_DATASTATUS_LIBS](https://docs.datacontroller.io/tables/mpe_datastatus_libs) - frequently changing attributes: library size, table count, catalog count
|
||||
|
||||
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
|
||||
|
||||
Refreshes are driven by the `mpe_refreshlibs` and `mpe_refreshtables` macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
|
||||
|
||||
Similar information is available for datasets - it's essentially an extract from `proc contents`, using the [mp_dsmeta](https://core.sasjs.io/mp__dsmeta_8sas.html) macro from the [SASjs Core](https://github.com/sasjs/core) library.
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
LinkedIn version (plain text - no markdown, links in comments):
|
||||
|
||||
One of many "SAS-isms" in Data Controller for SAS® - "Library Info"
|
||||
|
||||
When expanding a VIEW library, Data Catalog items are surfaced:
|
||||
|
||||
▪️ Library Engine
|
||||
▪️ Library Size (Bytes, MB, GB)
|
||||
▪️ Paths (if BASE engine)
|
||||
▪️ Table Count
|
||||
▪️ Schema (if a database)
|
||||
|
||||
And more. Much of this data can also be queried from the underlying tables (links in the comments below):
|
||||
|
||||
▪️ MPE_DATACATALOG_LIBS - engine, paths, permissions, owners, schemas, metadata name / id
|
||||
▪️ MPE_DATASTATUS_LIBS - frequently changing attributes: library size, table count, catalog count
|
||||
|
||||
The split is deliberate - stable attributes live in the DATACATALOG tables, volatile ones (size, counts) in DATASTATUS. All of it is stored with SCD2, so you can track how your data landscape changes over time.
|
||||
|
||||
Refreshes are driven by the mpe_refreshlibs and mpe_refreshtables macros - any user can refresh an individual library from the VIEW menu, and admins can scan the entire estate from the System page.
|
||||
|
||||
Similar information is available for datasets - it's essentially an extract from proc contents, using the mp_dsmeta macro from the SASjs Core library (link in the comments).
|
||||
|
||||
#sas #datacatalog #datagovernance #sasjs #metadata
|
||||
|
||||
Suggested first comment:
|
||||
|
||||
MPE_DATACATALOG_LIBS: https://docs.datacontroller.io/tables/mpe_datacatalog_libs
|
||||
MPE_DATASTATUS_LIBS: https://docs.datacontroller.io/tables/mpe_datastatus_libs
|
||||
mp_dsmeta macro: https://core.sasjs.io/mp__dsmeta_8sas.html
|
||||
|
||||
Attach library-info.jpeg (and optionally library-enginehost-info.jpeg + dataset-attributes.jpeg) to the post.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: 'Oops! Now You Can Roll Back Data Changes'
|
||||
description: Despite all the checks in Data Controller for SAS®, sometimes the wrong updates get approved. You can now roll back to a previous version - with full audit history preserved.
|
||||
date: '2026-08-20 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './rollback-trains-meme.jpeg'
|
||||
---
|
||||
|
||||
# Oops! Now You Can Roll Back Data Changes
|
||||
|
||||
Despite the MANY checks and guarantees in Data Controller for SAS®, sometimes it can happen that the wrong updates are approved and applied.
|
||||
|
||||
Thankfully, it is now possible to **roll back** data changes to a previous state.
|
||||
|
||||
## How it works in practice
|
||||
|
||||
Behind the scenes, rollback is not a silent undo. It is a **first-class approval workflow** just like any other edit. When you choose to restore a previous version, the backend - via the [`%mp_stripdiffs`](https://core.sasjs.io/mp__stripdiffs_8sas.html) macro - reads the `MPE_AUDIT` table (or a custom `AUDIT_LIBDS` configured for the table) and computes every difference between the current state and the version you want to go back to.
|
||||
|
||||
It handles all three change types:
|
||||
|
||||
- **Deleted rows** are re-inserted with their original values.
|
||||
- **Modified rows** are reverted to their previous values.
|
||||
- **Added rows** are marked for deletion with `_____DELETE__THIS__RECORD_____="Yes"`.
|
||||
|
||||
The computed differences are written to a new staging package in the approvals directory, complete with a CSV and a `macvars.sas` snapshot of the session context. A new `LOAD_REF` is generated, and the package is submitted via the standard `%mpe_loader` service - so it goes through the same edit-stage-approve workflow as any manual change.
|
||||
|
||||
This means the rollback itself is **reviewable and approvable**. Nothing is applied silently, and the full audit trail is maintained: the reversion appears as a new load reference in `MPE_SUBMIT`, `MPE_REVIEW`, `MPE_DATALOADS`, and `MPE_AUDIT`, just like any other submission.
|
||||
|
||||
## Security and access
|
||||
|
||||
Not everyone can roll back everything. The `%mpe_checkrestore` macro enforces a strict access check before the restore service will run:
|
||||
|
||||
- The load must actually exist and have been loaded (no rollbacks of unapproved submissions).
|
||||
- The table must be configured with an audit table.
|
||||
- The user must have `EDIT` access to the target table.
|
||||
- If the user is not an admin, Row Level Security or Column Level Security rules on the table will block the restore.
|
||||
|
||||
If access is denied, the service aborts with a clear reason - no opaque errors.
|
||||
|
||||
## What this means for compliance
|
||||
|
||||
Because the rollback creates a new, approved changeset rather than silently rewinding history, auditors can see exactly what was reverted, when, and by whom. The `MPE_AUDIT` table retains the record of every intermediate state, so nothing is ever truly lost. For tables where data integrity is critical - regulatory reporting, actuarial assumptions, steering parameters - this is the difference between "we have no idea what happened" and "here is the complete chain of custody."
|
||||
|
||||
This feature works for all temporal-aware load types (`UPDATE`, `TXTEMPORAL`, and `BITEMPORAL`) and respects SCD2 validity windows. And the whole process is built on the same open-source macro library that powers the rest of Data Controller - so you can inspect, test, and audit the code itself.
|
||||
|
||||
Full documentation is here: https://docs.datacontroller.io/rollback-data-changes/
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
"Which submission introduced this value?" should never be a hard question to answer.
|
||||
|
||||
Yet for many teams managing reference data, mappings, and regulatory adjustments in SAS®, a wrong approval means exactly that - an uncomfortable audit conversation, a forensic exercise, and a quiet hope that nobody upstream consumed the bad data.
|
||||
|
||||
Data Controller now lets you ROLL BACK a table to a previous state.
|
||||
|
||||
Not by silently rewinding history - the one thing your auditor definitely does not want. Instead, the reversion is packaged as a brand NEW change that goes through the same review and approval as any other edit:
|
||||
|
||||
- The reversion diff is calculated and staged
|
||||
- An approver reviews it before anything is applied
|
||||
- The rollback itself lands in the audit trail - who, what, when, why
|
||||
|
||||
Nothing disappears. The original mistake, its correction, and every state in between all remain fully traceable.
|
||||
|
||||
Access rules apply exactly as they do for edits - the same group permissions, the same restrictions. A rollback cannot be used as a shortcut around your controls.
|
||||
|
||||
For regulated reporting data - the kind where "oops" is a reportable event, not a shrug - this closes the loop between catching an error and evidencing its correction.
|
||||
|
||||
The underlying process is open source.
|
||||
|
||||
Link in the comments below 👇
|
||||
|
||||
#sas #sasviya #datagovernance #datamanagement
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
A dramatic flat-design meme illustration in the Data Controller brand style: two high-speed trains colliding head-on at the center of the frame, with data rows and spreadsheet cells flying out of the impact. A group of onlookers in business-casual attire stand in the foreground, heads bowed, looking on in shared sadness. Dark navy background, teal/green and muted orange accents. The collision represents conflicting data changes; the sad onlookers are the data stewards. Minimal text, no labels. Slightly stylised, not gory - suitable for a B2B data product audience. 16:9 landscape, suitable as a blog/feed cover image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 749 KiB |
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - Use Cases and Features'
|
||||
description: A rundown of Data Controller use cases and features - from secure data capture and approval workflows to SCD2 load routines, validation rules, and data lineage.
|
||||
date: '2026-08-12 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './usecases.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - Use Cases and Features
|
||||
|
||||
What can you actually *do* with Data Controller? Here's the rundown:
|
||||
|
||||
- Data viewer with search, filter, and URL links - without table locks
|
||||
- Secure data (stop end-user write access to production tables)
|
||||
- Configure inputs in seconds (vs days or weeks for ETL jobs)
|
||||
- Preconfigured load routines including SCD2 and Bitemporal
|
||||
- Avoid exposing sensitive data in unsecured staging areas
|
||||
- Enable SAS workflows (trigger a SAS job on data change)
|
||||
- Prevent spreadsheet changes from halting batch flows
|
||||
- Email alerts when data is changed / about to change
|
||||
- Audit manual overrides on regulatory reporting data
|
||||
- Four-eyes (or more) approval for all data changes
|
||||
- View and amend formats in SAS format catalogs
|
||||
- Export DDL in SAS, TSQL or PGSQL format
|
||||
- Explore SAS 9 metadata and data lineage
|
||||
- Work with special SAS missing numerics
|
||||
- Upload password-protected Excel files
|
||||
- Upload directly from CSV or Excel
|
||||
- Composite / retained key support
|
||||
- Enforce data quality at source
|
||||
- Column and Row Level Security
|
||||
- Advanced validation rules
|
||||
|
||||
Our software is used and trusted (in production) by:
|
||||
|
||||
- Governments
|
||||
- Insurers
|
||||
- Pharma
|
||||
- Banks
|
||||
|
||||
Plus various other industry sectors - references available.
|
||||
|
||||
Would you like to kick the tyres? [Get in touch](/contact) for details.
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Controller for SAS® Use Cases and Features:
|
||||
|
||||
✓ Data viewer with search, filter, and URL links - without table locks
|
||||
✓ Secure data (stop end-user write access to production tables)
|
||||
✓ Configure inputs in seconds (vs days or weeks for ETL jobs)
|
||||
✓ Preconfigured load routines including SCD2 and Bitemporal
|
||||
✓ Avoid exposing sensitive data in unsecured staging areas
|
||||
✓ Enable SAS workflows (trigger a SAS job on data change)
|
||||
✓ Prevent spreadsheet changes from halting batch flows
|
||||
✓ Email alerts when data is changed / about to change
|
||||
✓ Audit manual overrides on regulatory reporting data
|
||||
✓ Four-eyes (or more) approval for all data changes
|
||||
✓ View and amend formats in SAS format catalogs
|
||||
✓ Export DDL in SAS, TSQL or PGSQL format
|
||||
✓ Explore SAS 9 metadata and data lineage
|
||||
✓ Work with special SAS missing numerics
|
||||
✓ Upload password-protected Excel files
|
||||
✓ Upload directly from CSV or Excel
|
||||
✓ Composite / retained key support
|
||||
✓ Enforce data quality at source
|
||||
✓ Column and Row Level Security
|
||||
✓ Advanced validation rules
|
||||
|
||||
Our software is used and trusted (in production) by:
|
||||
|
||||
- Governments
|
||||
- Insurers
|
||||
- Pharma
|
||||
- Banks
|
||||
|
||||
Plus various other industry sectors - references available.
|
||||
|
||||
Would you like to kick the tires? DM for details.
|
||||
|
||||
#sas #sasviya #dataquality #datacapture
|
||||
|
||||
Image prompt:
|
||||
|
||||
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner. A clean modern data dashboard interface floating in the centre showing a data grid/table with a search bar and filter icons. Around it, a neat ring of small white and light-grey icons representing governance features: a padlock (security), a magnifying glass over rows (audit), two checkmarks side by side (4-eyes approval), an envelope (email alerts), an Excel/spreadsheet icon with an upward arrow (upload), a branching flow diagram (workflow/lineage), and a shield with a tick (data quality). Thin connecting lines link the icons to the central dashboard, suggesting control and flow. Minimal, professional, corporate SaaS style; subtle depth with soft shadows; accent colour a muted red-orange (#d3531a) used sparingly on one or two icons. No text, no words, no letters. High contrast, crisp edges, suitable as a LinkedIn post image.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 353 KiB |
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: 'v7.12.0: Regex Validation Rules'
|
||||
description: Define regex patterns once and have every value checked automatically - with hard rules that block bad data and soft rules to flag it for a second look.
|
||||
date: '2026-07-28 15:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
# v7.12.0: Regex Validation Rules
|
||||
|
||||
🚀 A new Data Controller release is out - now featuring **regex validation rules**.
|
||||
|
||||
Regex (short for "regular expression") is a way of describing a pattern that text should match - think "must look like an email address" or "must be a valid UK postcode". Instead of writing code to check every entry, you simply define the pattern once, and every value entered into that column is automatically checked against it. It's a technique trusted by developers and data teams for decades, now available to your rule authors in a simple, declarative way.
|
||||
|
||||
This release gives you two levels of enforcement:
|
||||
|
||||
- **Hard rules** (`HARDREGEX`) block submission entirely - an invalid email address simply can't be saved, and the cell turns red so the problem is obvious at a glance.
|
||||
- **Soft rules** (`SOFTREGEX`) show a gentle yellow warning instead, letting the submission through while flagging that something might need a second look. Perfect for checks that are important but not deal-breakers.
|
||||
|
||||
Rules are checked the moment they're created. Every pattern is run through SAS's own `PRXPARSE` engine when it's saved, so a broken or invalid regex is rejected immediately - with a clear message showing exactly which column it affects. There's no risk of a typo'd pattern silently failing on your users later.
|
||||
|
||||
The value? Cleaner data at the point of entry, with no manual review needed. Mistakes are caught the moment they're typed - not weeks later in a report - and your team decides exactly which rules are strict and which are advisory.
|
||||
|
||||
Full details in the [validation rules documentation](https://docs.datacontroller.io/dcc-validations/).
|
||||
|
||||
## Also in this release
|
||||
|
||||
🔒 Incompatible security settings are now caught before they cause problems
|
||||
|
||||
🔧 Table filters no longer leak between views
|
||||
|
||||
🛡️ Dependencies fully updated - zero known vulnerabilities, fully offline/on-prem capable
|
||||
|
||||
See the [v7.12.0 release notes](https://git.datacontroller.io/dc/dc/releases/tag/v7.12.0) for the complete changelog.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
title: "Welcome to the Data Controller Feed"
|
||||
description: A new home for quick announcements, social media updates and other bite-sized news from the Data Controller team.
|
||||
date: '2026-07-17 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
# Welcome to the Feed
|
||||
|
||||
We're launching a new **Feed** section on the Data Controller website! This is where you'll find:
|
||||
|
||||
- Quick announcements
|
||||
- Social media style updates
|
||||
- Smaller news items that don't warrant a full blog post
|
||||
|
||||
It sits alongside our [Blog](/blog/) and both are combined into a single [RSS feed](/rss.xml) so you can subscribe once and get everything in one place.
|
||||
|
||||
Stay tuned for more updates!
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: 'Zen and the Art of Data Quality'
|
||||
description: Pirsig's motorcycle classic meets the National Bank of Belgium's DQ guidance - and the three layers of data quality defence Data Controller for SAS provides.
|
||||
date: '2026-08-07 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
previewImg: './dilbert.jpeg'
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
# Zen and the Art of Data Quality
|
||||
|
||||
The [linked article below](https://www.linkedin.com/pulse/zen-art-data-quality-allan-bowe) contrasts Pirsig's 1974 motorcycle masterpiece with the insightful DQ guidance of the National Bank of Belgium ([nbb_2017_27](/blog/data-quality-and-the-nbb_2017_27-circular/)).
|
||||
|
||||

|
||||
|
||||
And it would be remiss not to explain how Data Controller for SAS® provides 3 layers of data quality defence (for those who Care):
|
||||
|
||||
1. **Frontend Defence** - select boxes, type checking, date pickers, validation rules, key protection. Prevents simple mistakes.
|
||||
|
||||
2. **Backend Defence** - run a SAS program following a submit to apply complex DQ rules, enabling immediate rejection with contextual reasoning. Prevents any mistake for which a rule can be written.
|
||||
|
||||
3. **Human Defence** - changes reviewed by one or more approvers before the target table is updated. The final line of defence against the stress of poor data quality.
|
||||
|
||||
To complete your data-zen experience - [Data Controller](https://git.datacontroller.io/dc/dc) can be downloaded, deployed AND configured in just a few minutes.
|
||||
|
||||
It runs on Viya, SAS 9 EBI, and vanilla Base SAS.
|
||||
|
||||
DM for details!
|
||||
|
||||
Read the full article: [Zen and the Art of Data Quality](https://www.linkedin.com/pulse/zen-art-data-quality-allan-bowe)
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
title: 'Zero-Code Business Data Capture for SAS®'
|
||||
description: Loading business data to IT-controlled databases should be simple, secure, and FAST. Data Controller for SAS® eliminates hand-cranked ETL flows.
|
||||
date: '2026-08-10 18:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './zero-code-business-data-capture-for-sas.jpeg'
|
||||
---
|
||||
|
||||
As a member of a Data & Analytics team in a regulated sector such as Banking or Insurance, you need to deliver outputs in a consistent and timely fashion to meet statutory and management reporting demands.
|
||||
|
||||
In order to do that, your team builds and maintains automated processes that capture, cleanse and integrate with centrally managed Data Warehouses.
|
||||
|
||||
The problem when capturing data created by business users (as opposed to IT-secured sources) is the cost to build, deploy and incident-manage ETL flows with constantly changing file formats and arrival frequencies.
|
||||
|
||||
This can result in 'blame-games' and delays in data delivery.
|
||||
|
||||
We strongly believe that loading business data to IT-controlled databases should be simple, secure, and fast.
|
||||
|
||||
That's why we created Data Controller for SAS® - a unique zero-code solution for capturing business data at source and moving it safely and securely into your IT-managed data store.
|
||||
|
||||
The result is the complete elimination of 'hand-cranked' ETL flows and other expensive home-grown solutions that eat up your OpEx budget and reduce your capacity to scale.
|
||||
|
||||
To see how easily Data Controller can be deployed in your organisation, book a [Discovery Call](https://datacontroller.io/contact/).
|
||||
|
After Width: | Height: | Size: 316 KiB |
@@ -0,0 +1,96 @@
|
||||
# Adding a Feed Post
|
||||
|
||||
The **Feed** section (`/feed/`) is for short announcements and social-media
|
||||
style updates — things that don't warrant a full [`content/blog/`](../content/blog)
|
||||
article, but are still worth publishing and syndicating via RSS.
|
||||
|
||||
Feed posts work exactly like blog posts (one markdown file per post, in its
|
||||
own directory), but live under `content/feed/` instead of `content/blog/`,
|
||||
and are published under `/feed/` instead of `/blog/`.
|
||||
|
||||
Both sections share a single combined RSS feed at
|
||||
[`/rss.xml`](https://datacontroller.io/rss.xml) (see `gatsby-plugin-feed` in
|
||||
[`gatsby-config.js`](../gatsby-config.js)), so a new Feed post automatically
|
||||
appears in the RSS feed the next time the site is built — no extra
|
||||
configuration required.
|
||||
|
||||
A visible "RSS Feed" link (pointing to `/rss.xml`) is rendered in two places
|
||||
so visitors can discover and subscribe to it:
|
||||
|
||||
- The site-wide [`Footer`](../src/components/footer/index.tsx), under
|
||||
"Other Resources".
|
||||
- The "Subscribe" section of the shared
|
||||
[`SideBar`](../src/templates/sidebar.tsx), shown on Blog and Feed list/post
|
||||
pages.
|
||||
|
||||
## 1. Create a new post directory
|
||||
|
||||
Create a new folder under `content/feed/`, named after the post slug (this
|
||||
becomes the URL):
|
||||
|
||||
```
|
||||
content/feed/my-new-announcement/index.md
|
||||
```
|
||||
|
||||
## 2. Write the frontmatter + content
|
||||
|
||||
```md
|
||||
---
|
||||
title: 'My New Announcement'
|
||||
description: A one or two sentence summary used for SEO and RSS.
|
||||
date: '2024-06-01 09:00:00'
|
||||
author: 'Your Name'
|
||||
authorLink: https://www.linkedin.com/in/yourprofile/
|
||||
tags:
|
||||
- Announcements
|
||||
---
|
||||
|
||||
Your announcement content goes here, written in regular Markdown.
|
||||
```
|
||||
|
||||
Notes on frontmatter fields:
|
||||
|
||||
| Field | Required | Notes |
|
||||
| ------------- | -------- | ---------------------------------------------------------------------- |
|
||||
| `title` | Yes | Post title, used on the page, in the sidebar and in the RSS item. |
|
||||
| `description` | Yes | Short summary, used for SEO meta tags and the RSS item description. |
|
||||
| `date` | Yes | Format `'YYYY-MM-DD HH:MM:SS'`. Controls sort order and the archive. |
|
||||
| `author` | Yes | Displayed under the post title. |
|
||||
| `authorLink` | No | If set, the author name links out (e.g. to a LinkedIn profile). |
|
||||
| `tags` | Yes | One or more tags. Used for the sidebar category list and RSS `<category>`. |
|
||||
| `previewImg` | No | Optional relative path to an image in the same folder (e.g. `'./cover.png'`). Feed posts commonly omit this since they're short-form. |
|
||||
|
||||
## 3. Add any images (optional)
|
||||
|
||||
If you use `previewImg` or reference images in the body, place the image
|
||||
files in the same post directory and reference them with a relative path,
|
||||
the same as for blog posts.
|
||||
|
||||
## 4. Build / preview
|
||||
|
||||
```shell
|
||||
npm run develop
|
||||
```
|
||||
|
||||
Your new post will be available at `/my-new-announcement/`, and will appear:
|
||||
|
||||
- On the [`/feed/`](https://datacontroller.io/feed/) listing page (with
|
||||
pagination, sidebar archives and tag categories under `/feed/...`).
|
||||
- In the site search index.
|
||||
- In the combined RSS feed at `/rss.xml`, alongside blog posts.
|
||||
|
||||
## How it differs from a Blog post
|
||||
|
||||
| Aspect | Blog | Feed |
|
||||
| ------------------ | ------------------------------ | ----------------------------------- |
|
||||
| Content directory | `content/blog/` | `content/feed/` |
|
||||
| Listing page | `/blog/` | `/feed/` |
|
||||
| Year archive | `/{year}/` | `/feed/{year}/` |
|
||||
| Category page | `/category/{tag}/` | `/feed/category/{tag}/` |
|
||||
| Template | `src/templates/blog-post.tsx`, `src/templates/blog-list.tsx` | `src/templates/feed-post.tsx`, `src/templates/feed-list.tsx` |
|
||||
| RSS | Included in `/rss.xml` | Included in `/rss.xml` |
|
||||
|
||||
Feed archive/category routes are prefixed with `/feed/` specifically to avoid
|
||||
colliding with the equivalent Blog routes, since both content types are
|
||||
rendered by the shared [`src/templates/sidebar.tsx`](../src/templates/sidebar.tsx)
|
||||
component (via its `basePath` prop).
|
||||
@@ -1,4 +1,5 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./src/styles/montserrat.css";
|
||||
|
||||
// Import all js dependencies.
|
||||
import "jquery/dist/jquery.min.js";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface SiteMetadata {
|
||||
title: string
|
||||
description: string
|
||||
siteUrl: string
|
||||
author: {
|
||||
name: string
|
||||
summary: string
|
||||
}
|
||||
social: {
|
||||
linkedin: string
|
||||
}
|
||||
}
|
||||
|
||||
export const siteMetadata: SiteMetadata
|
||||
export const pathPrefix: string
|
||||
@@ -8,14 +8,13 @@ module.exports = {
|
||||
summary: ``
|
||||
},
|
||||
social: {
|
||||
linkedin: `https://www.linkedin.com/showcase/data_controller/`
|
||||
linkedin: `https://www.linkedin.com/showcase/data-controller-for-sas/`
|
||||
}
|
||||
},
|
||||
pathPrefix: '',
|
||||
plugins: [
|
||||
'gatsby-plugin-styled-components',
|
||||
'gatsby-plugin-image',
|
||||
'gatsby-plugin-react-helmet',
|
||||
'gatsby-plugin-sitemap',
|
||||
{
|
||||
resolve: 'gatsby-plugin-manifest',
|
||||
@@ -23,7 +22,6 @@ module.exports = {
|
||||
name: `Data Controller | Flexible and Secure SAS® Data Modification`,
|
||||
short_name: `Data Controller`,
|
||||
description: `Data Controller for SAS® is dedicated to helping users, admins and developers manage their data. A zero code approach with Data Lineage, Catalog, Dictionary, Validation, Workflow, Alerts and more.`,
|
||||
homepage_url: 'https://datacontroller.io/',
|
||||
start_url: '/',
|
||||
background_color: '#fff',
|
||||
theme_color: '#314351',
|
||||
@@ -51,6 +49,14 @@ module.exports = {
|
||||
},
|
||||
__key: 'blog'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
path: `./content/feed`,
|
||||
name: `feed`
|
||||
},
|
||||
__key: 'feed'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
@@ -103,13 +109,6 @@ module.exports = {
|
||||
},
|
||||
__key: 'pages'
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-google-fonts`,
|
||||
options: {
|
||||
fonts: [`Montserrat\:300,400,500`],
|
||||
display: 'swap'
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-local-search',
|
||||
options: {
|
||||
@@ -131,7 +130,7 @@ module.exports = {
|
||||
// required.
|
||||
query: `
|
||||
{
|
||||
remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content/blog/"}}) {
|
||||
remark: allMarkdownRemark (filter: {fileAbsolutePath: {regex: "/content\\/(blog|feed)\\//"}}) {
|
||||
posts: edges {
|
||||
post: node {
|
||||
id
|
||||
@@ -184,6 +183,63 @@ module.exports = {
|
||||
html: data.post.html
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: 'gatsby-plugin-feed',
|
||||
options: {
|
||||
query: `
|
||||
{
|
||||
site {
|
||||
siteMetadata {
|
||||
title
|
||||
description
|
||||
siteUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
feeds: [
|
||||
{
|
||||
serialize: ({ query: { site, allMarkdownRemark } }) =>
|
||||
allMarkdownRemark.nodes.map((node) => ({
|
||||
title: node.frontmatter.title,
|
||||
description: node.frontmatter.description || '',
|
||||
date: node.frontmatter.date,
|
||||
url: site.siteMetadata.siteUrl + node.fields.slug,
|
||||
guid: site.siteMetadata.siteUrl + node.fields.slug,
|
||||
author: node.frontmatter.author,
|
||||
categories: node.frontmatter.tags || [],
|
||||
custom_elements: [{ 'content:encoded': node.html }]
|
||||
})),
|
||||
query: `
|
||||
{
|
||||
allMarkdownRemark(
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
filter: {
|
||||
fileAbsolutePath: { regex: "/content\\/(blog|feed)\\//" }
|
||||
}
|
||||
) {
|
||||
nodes {
|
||||
html
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
description
|
||||
date
|
||||
author
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
output: '/rss.xml',
|
||||
title: 'Data Controller — Blog & Feed'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ const recentPosts = []
|
||||
const archives = {}
|
||||
const tagsFrequent = []
|
||||
|
||||
const feedRecentPosts = []
|
||||
const feedArchives = {}
|
||||
const feedTagsFrequent = []
|
||||
|
||||
exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
const { createPage } = actions
|
||||
|
||||
@@ -14,12 +18,16 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
const blogListTemplate = path.resolve(`./src/templates/blog-list.tsx`)
|
||||
const blogSearchTemplate = path.resolve(`./src/templates/blog-search.tsx`)
|
||||
|
||||
// Define a template for feed post
|
||||
const feedPostTemplate = path.resolve(`./src/templates/feed-post.tsx`)
|
||||
const feedListTemplate = path.resolve(`./src/templates/feed-list.tsx`)
|
||||
|
||||
// Get all markdown blog posts sorted by date
|
||||
const result = await graphql(
|
||||
`
|
||||
{
|
||||
allMarkdownRemark(
|
||||
sort: { fields: [frontmatter___date], order: DESC }
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/blog/" } }
|
||||
) {
|
||||
@@ -34,8 +42,11 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
tagsGroup: allMarkdownRemark(limit: 1000) {
|
||||
group(field: frontmatter___tags) {
|
||||
tagsGroup: allMarkdownRemark(
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/blog/" } }
|
||||
) {
|
||||
group(field: { frontmatter: { tags: SELECT } }) {
|
||||
name: fieldValue
|
||||
totalCount
|
||||
}
|
||||
@@ -176,6 +187,166 @@ exports.createPages = async ({ graphql, actions, reporter }) => {
|
||||
tags: tagsFrequent
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Feed section (announcements / social updates) - mirrors the blog logic
|
||||
// above but is scoped to /content/feed/ and lives under the /feed/ path
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const feedResult = await graphql(
|
||||
`
|
||||
{
|
||||
allMarkdownRemark(
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/feed/" } }
|
||||
) {
|
||||
nodes {
|
||||
id
|
||||
fields {
|
||||
slug
|
||||
}
|
||||
frontmatter {
|
||||
title
|
||||
date(formatString: "YYYY")
|
||||
}
|
||||
}
|
||||
}
|
||||
tagsGroup: allMarkdownRemark(
|
||||
limit: 1000
|
||||
filter: { fileAbsolutePath: { regex: "/content/feed/" } }
|
||||
) {
|
||||
group(field: { frontmatter: { tags: SELECT } }) {
|
||||
name: fieldValue
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
if (feedResult.errors) {
|
||||
reporter.panicOnBuild(
|
||||
`There was an error loading your feed posts`,
|
||||
feedResult.errors
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const feedPosts = feedResult.data.allMarkdownRemark.nodes
|
||||
feedRecentPosts.push(
|
||||
...feedPosts.slice(0, 10).map((p) => ({
|
||||
slug: p.fields.slug,
|
||||
title: p.frontmatter.title
|
||||
}))
|
||||
)
|
||||
|
||||
const feedTags = feedResult.data.tagsGroup.group
|
||||
feedTagsFrequent.push(
|
||||
...feedTags.sort((a, b) => b.totalCount - a.totalCount).slice(0, 10)
|
||||
)
|
||||
|
||||
feedPosts.forEach((d) => {
|
||||
if (feedArchives[d.frontmatter.date] == null)
|
||||
feedArchives[d.frontmatter.date] = 0
|
||||
feedArchives[d.frontmatter.date]++
|
||||
})
|
||||
|
||||
// Create individual feed post pages
|
||||
if (feedPosts.length > 0) {
|
||||
feedPosts.forEach((post, index) => {
|
||||
const previousPostId = index === 0 ? null : feedPosts[index - 1].id
|
||||
const nextPostId =
|
||||
index === feedPosts.length - 1 ? null : feedPosts[index + 1].id
|
||||
|
||||
createPage({
|
||||
path: post.fields.slug,
|
||||
component: feedPostTemplate,
|
||||
context: {
|
||||
id: post.id,
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
previousPostId,
|
||||
nextPostId
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Create feed-list pages
|
||||
const feedNumPages = Math.ceil(feedPosts.length / postsPerPage)
|
||||
Array.from({ length: feedNumPages }).forEach((_, i) => {
|
||||
createPage({
|
||||
path: i === 0 ? `/feed` : `/feed/page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'index',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: { fileAbsolutePath: { regex: '/content/feed/' } },
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: feedNumPages,
|
||||
currentPage: i + 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
for (year in feedArchives) {
|
||||
const count = feedArchives[year]
|
||||
const numPagesOfYear = Math.ceil(count / postsPerPage)
|
||||
Array.from({ length: numPagesOfYear }).forEach((_, i) => {
|
||||
createPage({
|
||||
path:
|
||||
i === 0 ? `/feed/${year}/` : `/feed/${year}/page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'year',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: {
|
||||
frontmatter: { date: { gte: year, lt: year + 1 } },
|
||||
fileAbsolutePath: { regex: '/content/feed/' }
|
||||
},
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: numPagesOfYear,
|
||||
currentPage: i + 1,
|
||||
year: year
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
feedTags.forEach((tag) => {
|
||||
const count = tag.totalCount
|
||||
const numPagesOfTag = Math.ceil(count / postsPerPage)
|
||||
Array.from({ length: numPagesOfTag }).forEach((__, i) => {
|
||||
const tagPath = `/feed/category/${_.kebabCase(tag.name)}/`
|
||||
createPage({
|
||||
path: i === 0 ? tagPath : `${tagPath}page/${i + 1}`,
|
||||
component: feedListTemplate,
|
||||
context: {
|
||||
page: 'category',
|
||||
archives: feedArchives,
|
||||
recentPosts: feedRecentPosts,
|
||||
tags: feedTagsFrequent,
|
||||
filter: {
|
||||
frontmatter: { tags: { in: [tag.name] } },
|
||||
fileAbsolutePath: { regex: '/content/feed/' }
|
||||
},
|
||||
limit: postsPerPage,
|
||||
skip: i * postsPerPage,
|
||||
numPages: numPagesOfTag,
|
||||
currentPage: i + 1,
|
||||
tag: tag.name
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
exports.onCreatePage = ({ page, actions }) => {
|
||||
|
||||
@@ -14,34 +14,34 @@
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"lint": "npx prettier --check \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\"",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\" --ignore-path .gitignore"
|
||||
"lint:fix": "npx prettier --write \"src/**/*.+(ts|tsx|js|jsx|json|css|scss)\" --ignore-path .gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@browniebroke/gatsby-image-gallery": "^8.2.0",
|
||||
"@mdx-js/mdx": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"gatsby": "^5.13.6",
|
||||
"gatsby-plugin-google-analytics": "^5.13.1",
|
||||
"gatsby-plugin-google-fonts": "^1.0.1",
|
||||
"gatsby-plugin-image": "^3.13.1",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"gatsby": "^5.15.0",
|
||||
"gatsby-plugin-feed": "^5.16.0",
|
||||
"gatsby-plugin-image": "^3.15.0",
|
||||
"gatsby-plugin-local-search": "^2.0.1",
|
||||
"gatsby-plugin-manifest": "^5.13.1",
|
||||
"gatsby-plugin-matomo": "0.16.2",
|
||||
"gatsby-plugin-react-helmet": "^6.13.1",
|
||||
"gatsby-plugin-sharp": "^5.13.1",
|
||||
"gatsby-plugin-sitemap": "^6.13.1",
|
||||
"gatsby-plugin-styled-components": "^6.13.1",
|
||||
"gatsby-plugin-manifest": "^5.15.0",
|
||||
"gatsby-plugin-matomo": "^0.17.0",
|
||||
"gatsby-plugin-sharp": "^5.15.0",
|
||||
"gatsby-plugin-sitemap": "^6.15.0",
|
||||
"gatsby-plugin-styled-components": "^6.15.0",
|
||||
"gatsby-remark-embed-video": "^3.2.1",
|
||||
"gatsby-remark-images": "^7.13.1",
|
||||
"gatsby-remark-responsive-iframe": "^6.13.1",
|
||||
"gatsby-source-filesystem": "^5.13.1",
|
||||
"gatsby-transformer-remark": "^6.13.1",
|
||||
"gatsby-transformer-sharp": "^5.13.1",
|
||||
"gatsby-remark-images": "^7.15.0",
|
||||
"gatsby-remark-responsive-iframe": "^6.15.0",
|
||||
"gatsby-source-filesystem": "^5.15.0",
|
||||
"gatsby-transformer-remark": "^6.15.0",
|
||||
"gatsby-transformer-sharp": "^5.15.0",
|
||||
"nostr-tools": "^2.7.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-share": "^5.1.0",
|
||||
"react-use-flexsearch": "^0.1.1",
|
||||
@@ -50,7 +50,24 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^20.19.43",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"bootstrap": "^5.0.0-beta3",
|
||||
"jquery": "^3.6.0"
|
||||
"cloudron-surfer": "^6.4.1",
|
||||
"jquery": "^3.6.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.33.5",
|
||||
"msgpackr": "^1.12.1",
|
||||
"lmdb": "^2.9.4",
|
||||
"ordered-binary": "^1.6.1",
|
||||
"whatwg-url": "^17.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"grill-with-docs": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/grill-with-docs/SKILL.md",
|
||||
"computedHash": "9c460cbd94fd3c63cdef967dbdb6e66ca687103cdc380cd37834e4d10b738f78"
|
||||
},
|
||||
"setup-matt-pocock-skills": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/engineering/setup-matt-pocock-skills/SKILL.md",
|
||||
"computedHash": "74e894a3509e2676d4cdb771c8eace087092430635e845e02a9cc2f757c552a4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
// import dcLogo from "../../images/dclogo.png";
|
||||
|
||||
import { Container, Section } from '../shared'
|
||||
import { FaRss } from 'react-icons/fa'
|
||||
import { Section } from '../shared'
|
||||
import { SolidButton } from '../shared/styledComponents'
|
||||
import { StyledHeading, StyledDesc, InputStyled, StyledAnchor } from './style'
|
||||
import { siteMetadata } from '../../../gatsby-config'
|
||||
|
||||
const rssFeedUrl = `${siteMetadata.siteUrl}rss.xml`
|
||||
|
||||
const anchorStyles = {
|
||||
color: '#888'
|
||||
@@ -17,55 +17,39 @@ const Footer = () => (
|
||||
<div className="col-md-3 me-md-5">
|
||||
<StyledHeading>Data Controller</StyledHeading>
|
||||
<StyledDesc>
|
||||
Data Controller is a product of 4GL Apps, a brand of Bowe IO Ltd,
|
||||
which is a UK company with a focus on SAS Software,{' '}
|
||||
<StyledAnchor href="https://sasapps.io">Apps</StyledAnchor>, and
|
||||
Services.
|
||||
Data Controller is the product of a UK company with a singular focus
|
||||
on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
|
||||
.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Newsletter</StyledHeading>
|
||||
<form
|
||||
className="kwes-form"
|
||||
method="POST"
|
||||
action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga"
|
||||
>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="email"
|
||||
name="email"
|
||||
className="form-control"
|
||||
aria-describedby="emailHelp"
|
||||
placeholder="Email Address*"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="text"
|
||||
name="name"
|
||||
className="form-control"
|
||||
placeholder="First Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<InputStyled
|
||||
type="text"
|
||||
name="lastName"
|
||||
className="form-control"
|
||||
placeholder="Last Name"
|
||||
/>
|
||||
</div>
|
||||
<SolidButton>Subscribe</SolidButton>
|
||||
</form>
|
||||
<StyledHeading>Source Code</StyledHeading>
|
||||
<StyledDesc>
|
||||
Data Controller source is on our self-hosted{' '}
|
||||
<StyledAnchor href="https://git.datacontroller.io">
|
||||
Gitea Repository
|
||||
</StyledAnchor>
|
||||
; the underlying SASjs framework is on{' '}
|
||||
<StyledAnchor href="https://github.com/sasjs">GitHub</StyledAnchor>.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Other Resources</StyledHeading>
|
||||
<StyledDesc>
|
||||
Visit our educational and fun SAS® software quiz{' '}
|
||||
<StyledAnchor href="https://sasensei.com">Sasensei</StyledAnchor> and
|
||||
test your knowledge of SAS topics.
|
||||
Connect on{' '}
|
||||
<StyledAnchor href={siteMetadata.social.linkedin}>
|
||||
LinkedIn
|
||||
</StyledAnchor>
|
||||
, read the{' '}
|
||||
<StyledAnchor href="https://docs.datacontroller.io">
|
||||
docs
|
||||
</StyledAnchor>
|
||||
, or subscribe to the{' '}
|
||||
<StyledAnchor href={rssFeedUrl}>
|
||||
<FaRss style={{ marginRight: '6px', verticalAlign: 'middle' }} />
|
||||
RSS feed
|
||||
</StyledAnchor>{' '}
|
||||
for updates.
|
||||
</StyledDesc>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { StyledAnchorProps } from '../../types'
|
||||
|
||||
export const StyledHeading = styled.h6`
|
||||
margin-bottom: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
@@ -33,7 +35,10 @@ const Anchor = styled.a`
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledAnchor = ({ children, href }) => (
|
||||
export const StyledAnchor: React.FC<StyledAnchorProps> = ({
|
||||
children,
|
||||
href
|
||||
}) => (
|
||||
<Anchor href={href} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</Anchor>
|
||||
@@ -1,21 +1,15 @@
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import { Hero, HeroHeading, HeroDesc } from './style'
|
||||
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
|
||||
import { Container } from '../shared'
|
||||
|
||||
import { pathPrefix } from '../../../gatsby-config.js'
|
||||
import { pathPrefix } from '../../../gatsby-config'
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
heading: string
|
||||
desc: string
|
||||
}
|
||||
import type { HeroSectionProps } from '../../types'
|
||||
|
||||
const HeroSection: React.FC<PageProps<DataProps>> = ({
|
||||
const HeroSection: React.FC<HeroSectionProps> = ({
|
||||
location,
|
||||
heading,
|
||||
desc
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import styled from 'styled-components'
|
||||
import background from '../../images/home_hero_bg.png'
|
||||
|
||||
export const Hero = styled.main`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${(props) => (props.bg ? `url(${background})` : 'none')};
|
||||
background-attachment: scroll;
|
||||
background-position: bottom right;
|
||||
`
|
||||
|
||||
export const HeroHeading = styled.h1`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const HeroDesc = styled.p`
|
||||
opacity: 0.8;
|
||||
`
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Hero = styled.main<{ bg?: boolean }>`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
`
|
||||
|
||||
export const HeroHeading = styled.h1`
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const HeroDesc = styled.p`
|
||||
opacity: 0.8;
|
||||
`
|
||||
@@ -1,16 +1,12 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { PageProps } from 'gatsby'
|
||||
|
||||
import Navibar from './navibar'
|
||||
import HeroSection from './herosection'
|
||||
import Footer from './footer'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
heroSection: boolean
|
||||
}
|
||||
import type { LayoutProps } from '../types'
|
||||
|
||||
const Layout: React.FC<PageProps<DataProps>> = ({
|
||||
const Layout: React.FC<LayoutProps> = ({
|
||||
location,
|
||||
children,
|
||||
heroSection = true,
|
||||
|
||||
@@ -1,49 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Link, PageProps } from 'gatsby'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
import dcLogo from '../../images/dclogo.png'
|
||||
|
||||
import { Container } from '../shared'
|
||||
import { logoStyles, CustomNavBar, ulStyles, Li, StyledLink } from './style'
|
||||
|
||||
import { pathPrefix } from '../../../gatsby-config.js'
|
||||
import { pathPrefix } from '../../../gatsby-config'
|
||||
|
||||
const naviLinks = [
|
||||
import type { NavibarProps } from '../../types'
|
||||
|
||||
interface NaviLink {
|
||||
name: string
|
||||
url: string
|
||||
active: 'yes' | 'no'
|
||||
}
|
||||
|
||||
const naviLinks: NaviLink[] = [
|
||||
{ name: 'Home', url: '/', active: 'no' },
|
||||
{ name: 'About', url: '/about/', active: 'no' },
|
||||
{ name: 'Blog', url: '/blog/', active: 'no' },
|
||||
{ name: 'Feed', url: '/feed/', active: 'no' },
|
||||
{ name: 'FAQ', url: '/faq/', active: 'no' },
|
||||
{
|
||||
name: 'Home',
|
||||
url: '/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
url: '/about/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Blog',
|
||||
url: '/blog/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'FAQ',
|
||||
url: '/faq/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Documentation',
|
||||
name: 'Docs',
|
||||
url: 'https://docs.datacontroller.io/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Pricing',
|
||||
url: '/pricing/',
|
||||
active: 'no'
|
||||
},
|
||||
{
|
||||
name: 'Book Demo',
|
||||
url: '/contact/',
|
||||
active: 'no'
|
||||
},
|
||||
{ name: 'Pricing', url: '/pricing/', active: 'no' },
|
||||
{ name: 'Book Demo', url: '/contact/', active: 'no' },
|
||||
{
|
||||
name: 'Source Code',
|
||||
url: 'https://git.datacontroller.io/dc/dc',
|
||||
@@ -51,11 +36,7 @@ const naviLinks = [
|
||||
}
|
||||
]
|
||||
|
||||
type DataProps = {
|
||||
location: Location
|
||||
}
|
||||
|
||||
const Navibar: React.FC<PageProps<DataProps>> = ({ location }) => {
|
||||
const Navibar: React.FC<NavibarProps> = ({ location }) => {
|
||||
naviLinks.forEach((link) => (link.active = 'no'))
|
||||
const currentLink = naviLinks.find(
|
||||
(link) => pathPrefix + link.url === location?.pathname
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { Link } from 'gatsby'
|
||||
|
||||
// styles
|
||||
export const logoStyles = {
|
||||
height: '55px'
|
||||
}
|
||||
@@ -295,7 +294,6 @@ const LinkUnderlineStyles = css`
|
||||
transition: opacity 0.3s ease;
|
||||
`
|
||||
|
||||
// styled components
|
||||
export const Li = styled.li`
|
||||
position: relative;
|
||||
@media (min-width: 992px) {
|
||||
@@ -315,7 +313,14 @@ export const Li = styled.li`
|
||||
}
|
||||
}
|
||||
`
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
|
||||
interface StyledLinkProps {
|
||||
active?: string
|
||||
}
|
||||
|
||||
export const StyledLink = styled(({ active, ...rest }: any) => (
|
||||
<Link {...rest} />
|
||||
))<StyledLinkProps>`
|
||||
padding-right: 0.8rem !important;
|
||||
padding-left: 0.8rem !important;
|
||||
color: white !important;
|
||||
@@ -1,9 +1,15 @@
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useStaticQuery, graphql } from 'gatsby'
|
||||
|
||||
const Seo = ({ description, lang, meta, title, previewImg = undefined }) => {
|
||||
import type { SeoProps } from '../types'
|
||||
|
||||
const Seo: React.FC<SeoProps> = ({
|
||||
description = '',
|
||||
lang = 'en',
|
||||
meta = [],
|
||||
title = '',
|
||||
previewImg
|
||||
}) => {
|
||||
const { site } = useStaticQuery(graphql`
|
||||
query {
|
||||
site {
|
||||
@@ -31,50 +37,47 @@ const Seo = ({ description, lang, meta, title, previewImg = undefined }) => {
|
||||
? `${siteUrl}${previewImg}`
|
||||
: `${siteUrl}/img/data-controller.svg`
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang
|
||||
}}
|
||||
title={pageTitle}
|
||||
meta={[
|
||||
const metaTags = (
|
||||
[
|
||||
{ name: 'author', property: 'author', content: author },
|
||||
{
|
||||
name: 'description',
|
||||
property: 'og:description',
|
||||
content: metaDescription
|
||||
},
|
||||
// { name: 'facebook:site', content: '', },
|
||||
{ name: 'image', property: 'og:image', content: image },
|
||||
{
|
||||
name: `linkedin:site`,
|
||||
content: site.siteMetadata?.social?.linkedin || ``
|
||||
},
|
||||
{ name: `twitter:card`, content: `summary` },
|
||||
// { name: `twitter:creator`, content: site.siteMetadata?.social?.twitter || `` },
|
||||
{ name: `twitter:description`, content: metaDescription },
|
||||
// { name: 'twitter:site', content: `${site?.twitter}`, },
|
||||
{ name: `twitter:title`, content: title },
|
||||
// { name: 'youtube:site', content: `${site?.youtube}`, },
|
||||
{ property: `og:title`, content: title },
|
||||
{ property: `og:type`, content: `website` }
|
||||
].concat(meta)}
|
||||
] as Array<{ name?: string; property?: string; content?: string }>
|
||||
).concat(meta)
|
||||
|
||||
return (
|
||||
<>
|
||||
<html lang={lang} />
|
||||
<title>{pageTitle}</title>
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
title={`${defaultTitle} RSS Feed`}
|
||||
href={`${siteUrl}rss.xml`}
|
||||
/>
|
||||
{metaTags.map((tag, index) => (
|
||||
<meta
|
||||
key={tag.name || tag.property || index}
|
||||
name={tag.name}
|
||||
property={tag.property}
|
||||
content={tag.content}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Seo.defaultProps = {
|
||||
lang: `en`,
|
||||
meta: [],
|
||||
description: ``,
|
||||
title: ``
|
||||
}
|
||||
|
||||
Seo.propTypes = {
|
||||
description: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
meta: PropTypes.arrayOf(PropTypes.object),
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
export default Seo
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { PageProps } from 'gatsby'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
import type { ContainerProps } from '../../types'
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
@media (min-width: 576px) {
|
||||
@@ -13,6 +10,6 @@ const StyledDiv = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const Container: React.FC<PageProps<DataProps>> = ({ children }) => {
|
||||
export const Container: React.FC<ContainerProps> = ({ children }) => {
|
||||
return <StyledDiv className="container">{children}</StyledDiv>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||
|
||||
import { FaEnvelope } from 'react-icons/fa'
|
||||
|
||||
export const StyledLink = styled((props) => <Link {...props} />)`
|
||||
export const StyledLink = styled(Link)`
|
||||
color: rgb(255, 255, 255);
|
||||
background-color: rgb(144, 196, 69);
|
||||
border-radius: 0px;
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { PageProps } from 'gatsby'
|
||||
import { BottomSectionArrow } from './styledComponents'
|
||||
import { Container } from './'
|
||||
|
||||
type DataProps = {
|
||||
children?: React.ReactNode
|
||||
color?: string
|
||||
bgColor?: string
|
||||
bottomArrow?: boolean
|
||||
}
|
||||
import type { SectionProps } from '../../types'
|
||||
|
||||
const StyledSection = styled.div`
|
||||
const StyledSection = styled.div<{ color?: string; bgColor?: string }>`
|
||||
position: relative;
|
||||
padding: 50px 0;
|
||||
color: ${(props) => props.color || 'white'};
|
||||
background-color: ${(props) => props.bgColor || '#314351'};
|
||||
`
|
||||
|
||||
export const Section: React.FC<PageProps<DataProps>> = ({
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
children,
|
||||
bgColor,
|
||||
color,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import type { SolidButtonProps, OutlineButtonProps } from '../../types'
|
||||
|
||||
const BottomArrow = styled.div`
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
@@ -9,9 +11,6 @@ const BottomArrow = styled.div`
|
||||
background: inherit;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
left: 50%;
|
||||
// right: 0;
|
||||
// margin-left: auto;
|
||||
// margin-right: auto;
|
||||
z-index: 10;
|
||||
`
|
||||
const BottomArrowWrapper = styled.div`
|
||||
@@ -29,16 +28,17 @@ export const BottomSectionArrow = () => (
|
||||
</BottomArrowWrapper>
|
||||
)
|
||||
|
||||
export const SectionHeading = styled.h2`
|
||||
export const SectionHeading = styled.h2<{ center?: string }>`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
letter-spacing: 1px;
|
||||
font-weight: 400;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const SectionDesc = styled.p`
|
||||
export const SectionDesc = styled.p<{ center?: string; opacity?: string }>`
|
||||
text-align: ${(props) => (props.center === 'no' ? 'left' : 'center')};
|
||||
opacity: ${(props) => props.opacity ?? 0.6};
|
||||
a {
|
||||
@@ -59,16 +59,18 @@ const StyledSolidButton = styled.button`
|
||||
}
|
||||
`
|
||||
|
||||
export const SolidButton = ({
|
||||
export const SolidButton: React.FC<SolidButtonProps> = ({
|
||||
children,
|
||||
theme = 'light',
|
||||
type = 'submit',
|
||||
onClick = undefined
|
||||
disabled = false,
|
||||
onClick
|
||||
}) => (
|
||||
<StyledSolidButton
|
||||
type={type}
|
||||
className={`btn btn-${theme}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</StyledSolidButton>
|
||||
@@ -81,7 +83,7 @@ const StyledOutlineButton = styled.button`
|
||||
border-width: 2px;
|
||||
`
|
||||
|
||||
export const OutlineButton = ({ children }) => (
|
||||
export const OutlineButton: React.FC<OutlineButtonProps> = ({ children }) => (
|
||||
<StyledOutlineButton type="button" className="btn btn-outline-light">
|
||||
{children}
|
||||
</StyledOutlineButton>
|
||||
@@ -34,7 +34,7 @@ export class NostrController extends EventEmitter {
|
||||
/**
|
||||
* Function will publish provided event to the provided relays
|
||||
*/
|
||||
publishEvent = async (event: Event, relays: string[]) => {
|
||||
publishEvent = async (event: Event, relays: string[]): Promise<string[]> => {
|
||||
const simplePool = new SimplePool()
|
||||
const promises = simplePool.publish(relays, event)
|
||||
|
||||
@@ -47,7 +47,7 @@ export class NostrController extends EventEmitter {
|
||||
})
|
||||
|
||||
if (publishedRelays.length === 0) {
|
||||
const failedPublishes: any[] = []
|
||||
const failedPublishes: Array<{ relay: string; error: string }> = []
|
||||
const fallbackRejectionReason =
|
||||
'Attempt to publish an event has been rejected with unknown reason.'
|
||||
|
||||
@@ -92,7 +92,7 @@ export class NostrController extends EventEmitter {
|
||||
return Promise.resolve(signedEvent)
|
||||
}
|
||||
|
||||
nip04Encrypt = async (receiver: string, content: string) => {
|
||||
nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
|
||||
if (!this.generatedKeys) {
|
||||
throw new Error(`Private & public key pair is not found.`)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class NostrController extends EventEmitter {
|
||||
* @param isSigner Boolean indicating whether the recipient is a signer or viewer.
|
||||
* @param setAuthUrl Function to set the authentication URL in the component state.
|
||||
*/
|
||||
sendDM = async (pubkey: string, message: string) => {
|
||||
sendDM = async (pubkey: string, message: string): Promise<string[]> => {
|
||||
// Set up timeout promise to handle encryption timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
@@ -159,7 +159,7 @@ export class NostrController extends EventEmitter {
|
||||
* @param hexKey hex private or public key
|
||||
* @returns whether or not is key valid
|
||||
*/
|
||||
validateHex = (hexKey: string) => {
|
||||
validateHex = (hexKey: string): RegExpMatchArray | null => {
|
||||
return hexKey.match(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ export class NostrController extends EventEmitter {
|
||||
return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
|
||||
}
|
||||
|
||||
verifySignedEvent = (event: SignedEvent) => {
|
||||
verifySignedEvent = (event: SignedEvent): void => {
|
||||
const isGood = verifyEvent(event)
|
||||
|
||||
if (!isGood) {
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 482 KiB |
@@ -18,9 +18,10 @@ description: Data Controller for SAS® pricing starts from FREE, is fully TRA
|
||||
Data Controller is available in four packages, with no limits on number of users (since [v6](https://datacontroller.io/v6-0-api-explorer/)). Data Controller can run on ANY flavour of SAS - be that Viya, EBI, or [SASjs Server](https://server.sasjs.io).
|
||||
|
||||
| | Community | Bronze | Silver | Gold |
|
||||
|--------------------------|-----------------|-----------|-----------|------------|
|
||||
| ------------------------ | --------------- | ---------- | ---------- | ---------- |
|
||||
| Number of Users | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Environments | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Production Environments | Unlimited | 1 | 2 | Unlimited |
|
||||
| Dev / Test Environments | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Libraries | Unlimited | Unlimited | Unlimited | Unlimited |
|
||||
| Tables (per library) | 35 | 100 | Unlimited | Unlimited |
|
||||
| Rows (VIEW) | 15 | 100 | Unlimited | Unlimited |
|
||||
@@ -34,11 +35,10 @@ Data Controller is available in four packages, with no limits on number of users
|
||||
| Developer Days | ❌ | ❌ | ❌ | ✅ |
|
||||
| Consulting Days | ❌ | ❌ | ❌ | ✅ |
|
||||
| Perpetual Licence Option | ❌ | ❌ | ❌ | ✅ |
|
||||
| Price | FREE | 12k EUR pa | 25k EUR pa| Contact Us |
|
||||
| Price | FREE | 12k EUR pa | 21k EUR pa | Contact Us |
|
||||
|
||||
</br>
|
||||
<i>Data Controller Development Days (to build new product features etc) are available for all plans at a price of 600 EUR per day. Consulting rates are published <a href="https://sasapps.io/pricing">here</a>.</i>
|
||||
|
||||
<i>Data Controller Development Days (to build new product features etc) are available for all plans at a price of 600 EUR per day. Consulting rates are published <a target="_blank" rel="noopener" href="https://sasapps.io/pricing">here</a>.</i>
|
||||
|
||||
## Support
|
||||
|
||||
@@ -51,7 +51,6 @@ Tickets may be raised at either:
|
||||
1. Support Desk: https://support.4gl.io
|
||||
2. Support Mailbox: support@datacontroller.io
|
||||
|
||||
|
||||
The corresponding severity level should also be specified, eg as follows:
|
||||
|
||||
1. Level 1 - App is non-functional and cannot be used
|
||||
@@ -83,22 +82,21 @@ The following items are assumed:
|
||||
4. The client is able to provide relevant artefacts (logs, sample data, clarifications)
|
||||
|
||||
### Escalation
|
||||
|
||||
If a ticket cannot be resolved within the agreed SLA conditions, escalation has to be started by the assigned support department. Escalation will be made via video call between managers at both the client and 4GL.
|
||||
|
||||
Complaints on services should be directed to both the 4GL Engagement Manager and the client Application Owner.
|
||||
|
||||
|
||||
### In Scope Support
|
||||
|
||||
Support is unlimited, but restricted to items that actually relate to the tool (eg we cannot help with permissions, or data issues, beyond the guidance in the documentation). All support is provided remotely, using video calls / screen sharing technology.no
|
||||
|
||||
|
||||
Examples of ‘in-scope’ (not an exhaustive list):
|
||||
|
||||
* configuration & usage advice
|
||||
* troubleshooting
|
||||
* installation / deployment support
|
||||
* integration advice (actual integrations are a consulting effort)
|
||||
- configuration & usage advice
|
||||
- troubleshooting
|
||||
- installation / deployment support
|
||||
- integration advice (actual integrations are a consulting effort)
|
||||
|
||||
### Out of Scope Support
|
||||
|
||||
@@ -134,4 +132,3 @@ There may be situations where support cannot give immediate fixes, such as (but
|
||||
### Partner Purchases
|
||||
|
||||
Where Data Controller is purchased directly from one of our partners, the partner may provide first line support as per your agreement with them.
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
// import { Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -7,9 +6,14 @@ import Seo from '../components/seo'
|
||||
import { Section } from '../components/shared'
|
||||
import { SideBar } from '../templates/sidebar'
|
||||
|
||||
const NotFound = ({ data, location, pageContext }) => (
|
||||
import type { PageProps } from 'gatsby'
|
||||
import type { PageContext } from '../types'
|
||||
|
||||
const NotFound: React.FC<PageProps<unknown, PageContext>> = ({
|
||||
location,
|
||||
pageContext
|
||||
}) => (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo title="Page Not Found" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<SideBar
|
||||
pageContext={pageContext}
|
||||
@@ -21,3 +25,5 @@ const NotFound = ({ data, location, pageContext }) => (
|
||||
)
|
||||
|
||||
export default NotFound
|
||||
|
||||
export const Head = () => <Seo title="Page Not Found" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -10,118 +10,13 @@ import {
|
||||
SectionDesc
|
||||
} from '../components/shared/styledComponents'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardImg,
|
||||
CardBody,
|
||||
FeaturedImg,
|
||||
FeaturedDesc
|
||||
} from '../styledComponents/about'
|
||||
|
||||
import sasenseiLogo from '../images/sasapps/sasensei.png'
|
||||
import rubyLogo from '../images/sasapps/ruby.png'
|
||||
import pygrailLogo from '../images/sasapps/pygrail.png'
|
||||
import pilotLogo from '../images/sasapps/pilot.png'
|
||||
import yogaLogo from '../images/sasapps/yoga.png'
|
||||
import jsLogo from '../images/sasapps/js.png'
|
||||
import serbianLogo from '../images/sasapps/serbian.png'
|
||||
import quizrLogo from '../images/sasapps/quizr.png'
|
||||
import cdoquizLogo from '../images/sasapps/cdoquiz.png'
|
||||
import insureLogo from '../images/sasapps/insure.png'
|
||||
|
||||
import sasenseiFeatured from '../images/sasensei_featured.png'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SasApps = [
|
||||
{
|
||||
name: 'Sasensei',
|
||||
desc: 'Sasensei. SAS Software Quiz, Challenge, Compete & Learn',
|
||||
link: 'https://sasensei.com',
|
||||
themeColor: '#2196F3',
|
||||
logo: sasenseiLogo
|
||||
},
|
||||
{
|
||||
name: 'Ruby',
|
||||
desc: 'Ruby Cards - a Community Quiz Game for Rubyists',
|
||||
link: 'https://ruby.cards',
|
||||
themeColor: '#aa1502',
|
||||
logo: rubyLogo
|
||||
},
|
||||
{
|
||||
name: 'PyGrail',
|
||||
desc: 'PyGrail - a Community Quiz Game for Monty Pythonistas',
|
||||
link: 'https://pygrail.com',
|
||||
themeColor: '#efbf2c',
|
||||
logo: pygrailLogo
|
||||
},
|
||||
{
|
||||
name: 'Pilot',
|
||||
desc: 'Pilot Cards Leaderboard - How High Can You Climb?',
|
||||
link: 'https://pilot.cards',
|
||||
themeColor: '#446ff3',
|
||||
logo: pilotLogo
|
||||
},
|
||||
{
|
||||
name: 'Yoga.cards',
|
||||
desc: 'Yoga Cards - a Community Quiz Game for Student Yogis',
|
||||
link: 'https://yoga.cards',
|
||||
themeColor: '#6d69fb',
|
||||
logo: yogaLogo
|
||||
},
|
||||
{
|
||||
name: 'JS.cards',
|
||||
desc: 'JS Cards - a Community Quiz Game for JavaScript Ninjas',
|
||||
link: 'https://js.cards',
|
||||
themeColor: '#00cc99',
|
||||
logo: jsLogo
|
||||
},
|
||||
{
|
||||
name: 'Serbian.cards',
|
||||
desc: 'Serbian Cards - How Much Serbian Do You Know?',
|
||||
link: 'https://serbian.cards',
|
||||
themeColor: '#c6363c',
|
||||
logo: serbianLogo
|
||||
},
|
||||
{
|
||||
name: 'Quizr',
|
||||
desc: 'quizR - a community quiz game for useRs',
|
||||
link: 'https://quizr.io',
|
||||
themeColor: '#51904c',
|
||||
logo: quizrLogo
|
||||
},
|
||||
{
|
||||
name: 'CDOquiz',
|
||||
desc: 'CDO Quiz - a Royally Challenging Data Adventure',
|
||||
link: 'https://cdoquiz.com/',
|
||||
themeColor: '#a54499',
|
||||
logo: cdoquizLogo
|
||||
},
|
||||
{
|
||||
name: 'Insure.cards',
|
||||
desc: 'Insure Cards - Are You An Insurance Expert? Prove It',
|
||||
link: 'https://insure.cards/',
|
||||
themeColor: '#5f8ba5',
|
||||
logo: insureLogo
|
||||
}
|
||||
]
|
||||
|
||||
const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Behind Data Controller"
|
||||
desc="Data Controller is a product of 4GL, a brand of Bowe IO Ltd"
|
||||
>
|
||||
<Seo title="About" />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
@@ -132,79 +27,16 @@ const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
<br />
|
||||
The company supports the development of the Data Controller web
|
||||
application and other{' '}
|
||||
<a href="https://sasapps.io" title="SAS Apps">
|
||||
SAS Apps
|
||||
</a>
|
||||
, and the Macro Cards range of websites listed below:
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5">
|
||||
{SasApps.map((app) => (
|
||||
<div className="col">
|
||||
<Card
|
||||
className="card"
|
||||
href={app.link}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://sasapps.io"
|
||||
title="SAS Apps"
|
||||
>
|
||||
<CardImg
|
||||
className="card-img-top"
|
||||
src={app.logo}
|
||||
alt={app.name}
|
||||
title={app.name}
|
||||
/>
|
||||
<CardBody className="card-body">
|
||||
<h5 className="card-title">{app.name}</h5>
|
||||
<p className="card-text">{app.desc}</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<Section>
|
||||
<FeaturedImg src={sasenseiFeatured} alt="Sasensei" title="Sasensei" />
|
||||
<FeaturedDesc>
|
||||
Sasensei is a free question-based learning system dedicated to various
|
||||
aspects of the
|
||||
<br />
|
||||
SAS® software such as SAS® Programming, Administration, Risk
|
||||
Dimensions, Viya, AF/SCL, and much more.
|
||||
</FeaturedDesc>
|
||||
<FeaturedDesc>
|
||||
This SAS® quiz allows the player to test their knowledge, challenge
|
||||
their peers and submit their own questions for others to enjoy.
|
||||
<br />
|
||||
Sasensei is constantly improving by adding new features which
|
||||
contribute to the player experience and increase the educational
|
||||
value.
|
||||
</FeaturedDesc>
|
||||
</Section>
|
||||
<Section color="black" bgColor="#eaeaea" bottomArrow={false}>
|
||||
<div className="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5">
|
||||
<div className="col">
|
||||
1000+
|
||||
<br />
|
||||
Active Players
|
||||
</div>
|
||||
<div className="col">
|
||||
650+
|
||||
<br />
|
||||
Questions
|
||||
</div>
|
||||
<div className="col">
|
||||
20+
|
||||
<br />
|
||||
New Players Every Day
|
||||
</div>
|
||||
<div className="col">
|
||||
6<br />
|
||||
Unique Features
|
||||
</div>
|
||||
<div className="col">
|
||||
7<br />
|
||||
Difficulty Levels
|
||||
SAS Apps
|
||||
</a>
|
||||
.
|
||||
</SectionDesc>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -213,3 +45,5 @@ const About: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
}
|
||||
|
||||
export default About
|
||||
|
||||
export const Head = () => <Seo title="About" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React, { useState } from 'react'
|
||||
import { PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -22,24 +22,32 @@ import contactBg from '../images/contact_bg.jpg'
|
||||
import '../styledComponents/contact.css'
|
||||
import { NostrController } from '../controllers'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const Contact: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
const [name, setName] = useState<string>()
|
||||
const [email, setEmail] = useState<string>()
|
||||
const [subject, setSubject] = useState<string>()
|
||||
const [message, setMessage] = useState<string>()
|
||||
const [notification, setNotification] = useState<string>()
|
||||
const [name, setName] = React.useState<string>()
|
||||
const [email, setEmail] = React.useState<string>()
|
||||
const [emailError, setEmailError] = React.useState<boolean>(false)
|
||||
const [subject, setSubject] = React.useState<string>()
|
||||
const [message, setMessage] = React.useState<string>()
|
||||
const [notification, setNotification] = React.useState<string>()
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value
|
||||
setEmail(value)
|
||||
|
||||
if (
|
||||
value &&
|
||||
(value.toLowerCase().includes('@gmail.com') ||
|
||||
value.toLowerCase().includes('@googlemail.com'))
|
||||
) {
|
||||
setEmailError(true)
|
||||
setNotification('Gmail addresses are not allowed.')
|
||||
} else {
|
||||
setEmailError(false)
|
||||
setNotification(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const getBorderStyle = (value: string | undefined) =>
|
||||
value === undefined ? {} : value ? {} : { border: '1px solid red' }
|
||||
@@ -50,7 +58,6 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
heading="Book a Demo"
|
||||
desc="Schedule a demonstration of the Data Controller to see our latest features and explore use cases"
|
||||
>
|
||||
<Seo title="Contact" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
@@ -66,6 +73,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
onSubmit={async (evt) => {
|
||||
evt.preventDefault()
|
||||
|
||||
// Prevent sending if emailError is set
|
||||
if (emailError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (name && email && subject && message) {
|
||||
const res = await nostrController
|
||||
.sendDM(
|
||||
@@ -75,7 +87,7 @@ Email: ${email}
|
||||
Subject: ${subject}
|
||||
Message: ${message}`
|
||||
)
|
||||
.catch((err) => {
|
||||
.catch((err: unknown) => {
|
||||
setNotification(
|
||||
`Something went wrong. Please check the console for more information. Please try one more time.`
|
||||
)
|
||||
@@ -121,10 +133,7 @@ Message: ${message}`
|
||||
name="email"
|
||||
rules="required|email"
|
||||
aria-describedby="emailHelp"
|
||||
onChange={(evt) => {
|
||||
setEmail(evt.target.value)
|
||||
setNotification(undefined)
|
||||
}}
|
||||
onChange={handleEmailChange}
|
||||
style={getBorderStyle(email)}
|
||||
/>
|
||||
<div id="emailHelp" className="form-text">
|
||||
@@ -156,7 +165,7 @@ Message: ${message}`
|
||||
className="form-control contactFormStyles"
|
||||
id="mesage"
|
||||
name="message"
|
||||
rows="5"
|
||||
rows={5}
|
||||
rules="required|max:200"
|
||||
onChange={(evt) => {
|
||||
setMessage(evt.target.value)
|
||||
@@ -166,7 +175,9 @@ Message: ${message}`
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<SolidButton theme="dark">Submit</SolidButton>
|
||||
<SolidButton theme="dark" disabled={emailError}>
|
||||
Submit
|
||||
</SolidButton>
|
||||
</div>
|
||||
</form>
|
||||
{notification && <span>{notification}</span>}
|
||||
@@ -181,3 +192,5 @@ Message: ${message}`
|
||||
}
|
||||
|
||||
export default Contact
|
||||
|
||||
export const Head = () => <Seo title="Contact" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageProps, Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { PageProps, Link } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -13,24 +13,13 @@ import {
|
||||
|
||||
import { FAQ } from '../styledComponents/faq'
|
||||
|
||||
type DataProps = {
|
||||
site: {
|
||||
meta: {
|
||||
title: string
|
||||
description: string
|
||||
social: { linkedin: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Faq: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
const Faq: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading="Data Controller – FAQ"
|
||||
desc="Most common user questions and answers regarding the Data Controller"
|
||||
>
|
||||
<Seo title="FAQ" />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
@@ -77,3 +66,5 @@ const Faq: React.FC<PageProps<DataProps>> = ({ data, location }) => {
|
||||
}
|
||||
|
||||
export default Faq
|
||||
|
||||
export const Head = () => <Seo title="FAQ" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import { Link, graphql, PageProps } from 'gatsby'
|
||||
import Gallery from '@browniebroke/gatsby-image-gallery'
|
||||
import { IGatsbyImageData } from 'gatsby-plugin-image'
|
||||
|
||||
@@ -34,31 +34,31 @@ import dcDesign from '../images/data-controller-design.png'
|
||||
|
||||
interface ImageSharpEdge {
|
||||
node: {
|
||||
name: string
|
||||
childImageSharp: {
|
||||
thumb: IGatsbyImageData
|
||||
full: IGatsbyImageData
|
||||
meta: {
|
||||
originalName: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
data: {
|
||||
interface IndexPageData {
|
||||
images: {
|
||||
edges: ImageSharpEdge[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Home: React.FC<PageProps> = ({ data, location }) => {
|
||||
const CustomWrapper: React.FC<{
|
||||
children?: React.ReactNode
|
||||
onClick?: () => void
|
||||
}> = ({ children, onClick }) => (
|
||||
<ThumbnailWrapper onClick={onClick}>{children}</ThumbnailWrapper>
|
||||
)
|
||||
|
||||
const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
||||
const images = data.images.edges.map(({ node }) => ({
|
||||
...node.childImageSharp,
|
||||
// Use original name as caption.
|
||||
// The `originalName` is queried in a nested field,
|
||||
// but the `Gallery` component expects `caption` at the top level.
|
||||
caption: node.childImageSharp.meta.originalName
|
||||
caption: node.name
|
||||
}))
|
||||
|
||||
return (
|
||||
@@ -67,7 +67,6 @@ const Home: React.FC<PageProps> = ({ data, location }) => {
|
||||
heading="Flexible and Secure Data Modification"
|
||||
desc="Data Controller for SAS® enables business users to update IT owned source tables, secured by review / approve workflow and full audit trail."
|
||||
>
|
||||
<Seo />
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
@@ -231,13 +230,7 @@ const Home: React.FC<PageProps> = ({ data, location }) => {
|
||||
</Section>
|
||||
<Section color="black" bgColor="white">
|
||||
<SectionHeading>See How it Looks</SectionHeading>
|
||||
<Gallery
|
||||
images={images}
|
||||
gutter={0}
|
||||
customWrapper={({ children, onClick }) => (
|
||||
<ThumbnailWrapper onClick={onClick}>{children}</ThumbnailWrapper>
|
||||
)}
|
||||
/>
|
||||
<Gallery images={images} gutter="0" customWrapper={CustomWrapper} />
|
||||
</Section>
|
||||
<ScheduleDemo />
|
||||
</Layout>
|
||||
@@ -250,10 +243,11 @@ export const pageQuery = graphql`
|
||||
query ImagesForGallery {
|
||||
images: allFile(
|
||||
filter: { relativeDirectory: { eq: "gallery" } }
|
||||
sort: { fields: name }
|
||||
sort: { name: ASC }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
childImageSharp {
|
||||
thumb: gatsbyImageData(
|
||||
width: 300
|
||||
@@ -261,12 +255,11 @@ export const pageQuery = graphql`
|
||||
placeholder: BLURRED
|
||||
)
|
||||
full: gatsbyImageData(layout: FULL_WIDTH)
|
||||
meta: fixed {
|
||||
originalName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const Head = () => <Seo />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { graphql } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
@@ -7,6 +7,8 @@ import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
color: rgb(102, 102, 102);
|
||||
h2,
|
||||
@@ -18,20 +20,26 @@ const StyledMarkdown = styled.div`
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica,
|
||||
Arial, sans-serif;
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Template({ data, location }) {
|
||||
const { frontmatter, html } = data?.markdownRemark
|
||||
interface MarkdownPageData {
|
||||
markdownRemark: PostNode
|
||||
}
|
||||
|
||||
const MarkdownPageTemplate: React.FC<
|
||||
PageProps<MarkdownPageData, PageContext>
|
||||
> = ({ data, location }) => {
|
||||
const { frontmatter, html } = data.markdownRemark
|
||||
return (
|
||||
<Layout
|
||||
location={location}
|
||||
heading={frontmatter.heading}
|
||||
desc={frontmatter.desc}
|
||||
>
|
||||
<Seo title={frontmatter.title} />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<StyledMarkdown dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</Section>
|
||||
@@ -39,6 +47,12 @@ export default function Template({ data, location }) {
|
||||
)
|
||||
}
|
||||
|
||||
export default MarkdownPageTemplate
|
||||
|
||||
export const Head: React.FC<{ data: MarkdownPageData }> = ({ data }) => (
|
||||
<Seo title={data.markdownRemark.frontmatter.title} />
|
||||
)
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query ($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h3`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
export const ContactBackground = styled.img.attrs((props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
}))`
|
||||
max-width: 100%;
|
||||
`
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const StyledHeading = styled.h3`
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
export const ContactBackground = styled.img.attrs<{ info?: string }>(
|
||||
(props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
})
|
||||
)`
|
||||
max-width: 100%;
|
||||
`
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { FcFaq } from 'react-icons/fc'
|
||||
|
||||
import type { FaqProps } from '../types'
|
||||
|
||||
const StyledFaq = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StyledQuestion = styled.h5`
|
||||
color: #222;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
@@ -33,7 +36,7 @@ const FaqDetails = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export const FAQ = ({ question, answer }) => (
|
||||
export const FAQ: React.FC<FaqProps> = ({ question, answer }) => (
|
||||
<StyledFaq>
|
||||
<IconHolderStyled>
|
||||
<FcFaq size={40} />
|
||||
@@ -1,6 +1,9 @@
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ImArrowRight } from 'react-icons/im'
|
||||
import type { IconType } from 'react-icons'
|
||||
|
||||
import type { ArtProps, ReasonProps, FeatureProps } from '../types'
|
||||
|
||||
export const CenteredText = styled.p`
|
||||
text-align: center;
|
||||
@@ -9,20 +12,19 @@ export const CenteredText = styled.p`
|
||||
margin: 0 auto;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
`
|
||||
export const Art = styled.img.attrs((props) => ({
|
||||
export const Art = styled.img.attrs<ArtProps>((props) => ({
|
||||
alt: props.info || '',
|
||||
title: props.info || ''
|
||||
}))`
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
height: auto;
|
||||
`
|
||||
|
||||
const ArrowHolder = styled.div`
|
||||
const ArrowHolder = styled.div<{ bgColor?: string }>`
|
||||
display: inline-block;
|
||||
border-radius: 50px;
|
||||
padding: 15px 17px;
|
||||
@@ -30,7 +32,7 @@ const ArrowHolder = styled.div`
|
||||
background-color: ${(props) =>
|
||||
props.bgColor === 'red' ? '#ed1c1c' : '#90c445'};
|
||||
`
|
||||
const RightArrow = ({ bgColor = '' }) => (
|
||||
const RightArrow: React.FC<{ bgColor?: string }> = ({ bgColor = '' }) => (
|
||||
<ArrowHolder bgColor={bgColor}>
|
||||
<ImArrowRight size={24} />
|
||||
</ArrowHolder>
|
||||
@@ -46,7 +48,7 @@ const ReasonDesc = styled.p`
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
export const Reason = ({ text, bgColor = '' }) => (
|
||||
export const Reason: React.FC<ReasonProps> = ({ text, bgColor = '' }) => (
|
||||
<>
|
||||
<RightArrow bgColor={bgColor} />
|
||||
<ReasonDesc>{text}</ReasonDesc>
|
||||
@@ -62,7 +64,7 @@ const IconHolderStyled = styled.div`
|
||||
vertical-align: top;
|
||||
`
|
||||
|
||||
const IconHolder = ({ Icon }) => {
|
||||
const IconHolder = ({ Icon }: { Icon: IconType }) => {
|
||||
return (
|
||||
<IconHolderStyled>
|
||||
<Icon size={24} />
|
||||
@@ -77,7 +79,8 @@ const FeatureDetails = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
const FeatureTitle = styled.h5`
|
||||
font-family: 'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
font-family:
|
||||
'Montserrat', 'HelveticaNeue', 'Helvetica Neue', Helvetica, Arial,
|
||||
sans-serif;
|
||||
text-transform: uppercase;
|
||||
`
|
||||
@@ -86,7 +89,12 @@ const FeatureDesc = styled.p`
|
||||
font-size: 0.9rem;
|
||||
`
|
||||
|
||||
export const Feature = ({ title, desc, Icon, className }) => (
|
||||
export const Feature: React.FC<FeatureProps> = ({
|
||||
title,
|
||||
desc,
|
||||
Icon,
|
||||
className
|
||||
}) => (
|
||||
<div className={className ?? 'col-md-4'}>
|
||||
<IconHolder Icon={Icon} />
|
||||
<FeatureDetails>
|
||||
@@ -0,0 +1,35 @@
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-300.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-400.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/Montserrat-500.woff2') format('woff2');
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
|
||||
U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import React from 'react'
|
||||
import { Link, graphql, PageProps } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import kebabCase from 'lodash/kebabCase'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -16,27 +16,39 @@ import {
|
||||
import Post from './postpreview'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
const BlogListTemplate = ({ data, location, pageContext }) => {
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
interface BlogListData {
|
||||
remark: {
|
||||
posts: Array<{ post: PostNode }>
|
||||
}
|
||||
}
|
||||
|
||||
const BlogListTemplate: React.FC<PageProps<BlogListData, PageContext>> = ({
|
||||
data,
|
||||
location,
|
||||
pageContext
|
||||
}) => {
|
||||
const posts = data.remark.posts
|
||||
|
||||
const iniPath =
|
||||
pageContext.page == 'index'
|
||||
const iniPath: string =
|
||||
pageContext.page === 'index'
|
||||
? `/blog/`
|
||||
: pageContext.page == 'year'
|
||||
: pageContext.page === 'year'
|
||||
? `/${pageContext.year}/`
|
||||
: pageContext.page == 'category'
|
||||
? `/category/${kebabCase(pageContext.tag)}/`
|
||||
: null
|
||||
: pageContext.page === 'category'
|
||||
? `/category/${kebabCase(pageContext.tag ?? '')}/`
|
||||
: `/blog/`
|
||||
|
||||
const pageInfo = `Page ${pageContext.currentPage} of ${pageContext.numPages}`
|
||||
Array.from({ length: 5 }, (v, k) => k + 1)
|
||||
const paginationJSX = Array.from(
|
||||
{ length: pageContext.numPages },
|
||||
{ length: pageContext.numPages ?? 0 },
|
||||
(_, i) => i + 1
|
||||
).map((pageIndex) => {
|
||||
const link = pageIndex === 1 ? iniPath : `${iniPath}page/${pageIndex}`
|
||||
return (
|
||||
<Link
|
||||
key={pageIndex}
|
||||
to={link}
|
||||
className={`btn btn-outline-dark btn-sm ${
|
||||
pageIndex === pageContext.currentPage ? 'disabled' : ''
|
||||
@@ -57,13 +69,12 @@ const BlogListTemplate = ({ data, location, pageContext }) => {
|
||||
heading="Data Controller’s Knowledge Base"
|
||||
desc="A section dedicated to news, updates and educational pieces."
|
||||
>
|
||||
<Seo title="Blog" />
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
{posts.map((data, i) => (
|
||||
<Post key={i} post={data.post} />
|
||||
{posts.map(({ post }, i) => (
|
||||
<Post key={i} post={post} />
|
||||
))}
|
||||
</div>
|
||||
<span className="float-start">{paginationJSX}</span>
|
||||
@@ -90,7 +101,7 @@ export const pageQuery = graphql`
|
||||
filter: $filter
|
||||
limit: $limit
|
||||
skip: $skip
|
||||
sort: { fields: [frontmatter___date], order: DESC }
|
||||
sort: { frontmatter: { date: DESC } }
|
||||
) {
|
||||
posts: edges {
|
||||
post: node {
|
||||
@@ -115,4 +126,4 @@ export const pageQuery = graphql`
|
||||
}
|
||||
`
|
||||
|
||||
// tags
|
||||
export const Head = () => <Seo title="Blog" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Link, graphql } from 'gatsby'
|
||||
import * as React from 'react'
|
||||
import { graphql, PageProps } from 'gatsby'
|
||||
|
||||
import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
@@ -8,19 +8,15 @@ import { Section } from '../components/shared'
|
||||
import Post from './post'
|
||||
import { SideBar } from './sidebar'
|
||||
|
||||
const BlogPostTemplate = ({ data, location, pageContext }) => {
|
||||
import type { PostNode, PageContext } from '../types'
|
||||
|
||||
const BlogPostTemplate: React.FC<
|
||||
PageProps<{ post: PostNode }, PageContext>
|
||||
> = ({ data, location, pageContext }) => {
|
||||
const { post } = data
|
||||
const { previewImg } = post.frontmatter
|
||||
|
||||
return (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Seo
|
||||
title={post?.frontmatter?.title}
|
||||
description={post?.frontmatter?.description}
|
||||
previewImg={
|
||||
previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src
|
||||
}
|
||||
/>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
@@ -37,6 +33,21 @@ const BlogPostTemplate = ({ data, location, pageContext }) => {
|
||||
|
||||
export default BlogPostTemplate
|
||||
|
||||
export const Head: React.FC<{ data: { post: PostNode } }> = ({ data }) => {
|
||||
const { post } = data
|
||||
const { previewImg } = post.frontmatter
|
||||
|
||||
return (
|
||||
<Seo
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.description}
|
||||
previewImg={
|
||||
previewImg?.childImageSharp?.gatsbyImageData?.images?.fallback?.src
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query PostByPath($id: String!) {
|
||||
post: markdownRemark(id: { eq: $id }) {
|
||||
|
||||