Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f63e507ddf | ||
| 991cc0567d | |||
| 52d58036a4 | |||
| 26bff85792 | |||
| 2ccf0d1100 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,3 +1,15 @@
|
|||||||
|
# [7.6.0](https://git.datacontroller.io/dc/dc/compare/v7.5.0...v7.6.0) (2026-04-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add label and tooltip for libref download, sanitise input ([52d5803](https://git.datacontroller.io/dc/dc/commit/52d58036a40e25847e900f9b04a77dbcc409c12b))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* configurable email alerts. Closes [#217](https://git.datacontroller.io/dc/dc/issues/217) ([2ccf0d1](https://git.datacontroller.io/dc/dc/commit/2ccf0d11000129629a0665421135b7530af9892f))
|
||||||
|
|
||||||
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)
|
# [7.5.0](https://git.datacontroller.io/dc/dc/compare/v7.4.1...v7.5.0) (2026-04-03)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import Handsontable from 'handsontable'
|
import Handsontable from 'handsontable'
|
||||||
import { Subject, Subscription } from 'rxjs'
|
import { Subject, Subscription } from 'rxjs'
|
||||||
|
import { sanitiseForSas } from '../shared/utils/sanitise'
|
||||||
import { SasStoreService } from '../services/sas-store.service'
|
import { SasStoreService } from '../services/sas-store.service'
|
||||||
|
|
||||||
type AOA = any[][]
|
type AOA = any[][]
|
||||||
@@ -1669,7 +1670,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.submit = true
|
this.submit = true
|
||||||
const updateParams: any = {}
|
const updateParams: any = {}
|
||||||
updateParams.ACTION = 'LOAD'
|
updateParams.ACTION = 'LOAD'
|
||||||
this.message = this.message.replace(/\n/g, '. ')
|
this.message = sanitiseForSas(this.message.replace(/\n/g, '. '))
|
||||||
updateParams.MESSAGE = this.message
|
updateParams.MESSAGE = this.message
|
||||||
// updateParams.APPROVER = this.approver;
|
// updateParams.APPROVER = this.approver;
|
||||||
updateParams.LIBDS = this.libds
|
updateParams.LIBDS = this.libds
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
|
import { sanitiseForSas } from '../../shared/utils/sanitise'
|
||||||
import { SasStoreService } from '../../services/sas-store.service'
|
import { SasStoreService } from '../../services/sas-store.service'
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -136,7 +137,7 @@ export class ApproveDetailsComponent implements AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
public async rejecting() {
|
public async rejecting() {
|
||||||
this.rejectLoading = true
|
this.rejectLoading = true
|
||||||
this.submitReason = this.submitReason.replace(/\n/g, '. ')
|
this.submitReason = sanitiseForSas(this.submitReason.replace(/\n/g, '. '))
|
||||||
|
|
||||||
let rejParams = {
|
let rejParams = {
|
||||||
STP_ACTION: 'REJECT_TABLE',
|
STP_ACTION: 'REJECT_TABLE',
|
||||||
|
|||||||
6
client/src/app/shared/utils/sanitise.ts
Normal file
6
client/src/app/shared/utils/sanitise.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Strips characters that could cause SAS macro injection (& % ;).
|
||||||
|
*/
|
||||||
|
export function sanitiseForSas(input: string): string {
|
||||||
|
return input.replace(/[%&;]/g, '')
|
||||||
|
}
|
||||||
@@ -236,14 +236,31 @@
|
|||||||
<div class="admin-action">
|
<div class="admin-action">
|
||||||
Download Configuration
|
Download Configuration
|
||||||
|
|
||||||
<input
|
<div class="libref-group">
|
||||||
type="text"
|
<clr-tooltip class="libref-tooltip">
|
||||||
class="clr-input libref-input"
|
<label clrTooltipTrigger class="libref-label">
|
||||||
maxlength="8"
|
Target DC Library
|
||||||
[ngModel]="dcLib"
|
<cds-icon shape="info-circle" size="16"></cds-icon>
|
||||||
(ngModelChange)="targetLibref = $event.toUpperCase()"
|
</label>
|
||||||
placeholder="Target Libref"
|
<clr-tooltip-content
|
||||||
/>
|
clrPosition="bottom-left"
|
||||||
|
clrSize="md"
|
||||||
|
*clrIfOpen
|
||||||
|
>
|
||||||
|
Enter the target DC library and the downloaded files will
|
||||||
|
contain this, instead of the original.
|
||||||
|
</clr-tooltip-content>
|
||||||
|
</clr-tooltip>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="clr-input libref-input"
|
||||||
|
maxlength="8"
|
||||||
|
[ngModel]="dcLib"
|
||||||
|
(ngModelChange)="targetLibref = $event.toUpperCase()"
|
||||||
|
placeholder="e.g. MYLIB"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
(click)="downloadConfiguration()"
|
(click)="downloadConfiguration()"
|
||||||
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
|
[disabled]="targetLibref !== dcLib && !isValidLibref(targetLibref)"
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
|
.libref-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.libref-label {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--clr-p4-color, #565656);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.libref-input {
|
.libref-input {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
margin: 0 8px;
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dcfrontend",
|
"name": "dcfrontend",
|
||||||
"version": "7.5.0",
|
"version": "7.6.0",
|
||||||
"description": "Data Controller",
|
"description": "Data Controller",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@saithodev/semantic-release-gitea": "^2.1.0",
|
"@saithodev/semantic-release-gitea": "^2.1.0",
|
||||||
|
|||||||
52
sas/sasjs/db/migrations/20260403_v7.6_release.sas
Normal file
52
sas/sasjs/db/migrations/20260403_v7.6_release.sas
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
@file
|
||||||
|
@brief migration script to move from v7.0 to v7.6 of data controller
|
||||||
|
|
||||||
|
OPTIONAL CHANGE - upload additional data as placeholders for modifying the
|
||||||
|
default email message
|
||||||
|
|
||||||
|
**/
|
||||||
|
|
||||||
|
%let dclib=YOURDCLIB;
|
||||||
|
|
||||||
|
libname &dclib "/YOUR/DATACONTROLLER/LIBRARY/PATH";
|
||||||
|
|
||||||
|
proc sql;
|
||||||
|
insert into &dclib..mpe_config set
|
||||||
|
tx_from=%sysfunc(datetime())
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="SUBMITTED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||||
|
!!'%superq(SUBMITTED_TXT)'
|
||||||
|
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||||
|
!!'documentation, please visit https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after submitting a change';
|
||||||
|
insert into &dclib..mpe_config set
|
||||||
|
tx_from=%sysfunc(datetime())
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="APPROVED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
|
||||||
|
!!' Controller for SAS. For documentation, please visit '
|
||||||
|
!!'https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after approving a change';
|
||||||
|
insert into &dclib..mpe_config set
|
||||||
|
tx_from=%sysfunc(datetime())
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="REJECTED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||||
|
!!'%superq(REVIEW_REASON_TXT)'
|
||||||
|
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||||
|
!!'documentation, please visit https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after rejecting a change';
|
||||||
@@ -127,6 +127,11 @@ run;
|
|||||||
filename __out email (&emails)
|
filename __out email (&emails)
|
||||||
subject="Table &alert_lib..&alert_ds has been &alert_event";
|
subject="Table &alert_lib..&alert_ds has been &alert_event";
|
||||||
|
|
||||||
|
data work.alertmessage;
|
||||||
|
set &mpelib..mpe_config;
|
||||||
|
where &dc_dttmtfmt. lt tx_to;
|
||||||
|
where also var_scope='DC_EMAIL' and var_name="&alert_event._TEMPLATE";
|
||||||
|
run;
|
||||||
%local SUBMITTED_TXT;
|
%local SUBMITTED_TXT;
|
||||||
%if &alert_event=SUBMITTED %then %do;
|
%if &alert_event=SUBMITTED %then %do;
|
||||||
data _null_;
|
data _null_;
|
||||||
@@ -136,30 +141,54 @@ filename __out email (&emails)
|
|||||||
run;
|
run;
|
||||||
data _null_;
|
data _null_;
|
||||||
File __out lrecl=32000;
|
File __out lrecl=32000;
|
||||||
|
length txt $2048;
|
||||||
|
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||||
put 'Dear user,';
|
put 'Dear user,';
|
||||||
put ' ';
|
put ' ';
|
||||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||||
"been proposed by &from_user on the '&syshostname' SAS server.";
|
"been proposed by &from_user on the &syshostname SAS server.";
|
||||||
put " ";
|
put " ";
|
||||||
length txt $2048;
|
|
||||||
txt=symget('SUBMITTED_TXT');
|
txt=symget('SUBMITTED_TXT');
|
||||||
put "Reason provided: " txt;
|
put "Reason provided: " txt;
|
||||||
put " ";
|
put " ";
|
||||||
put "This is an automated email by Data Controller for SAS. For "
|
put "This is an automated email by Data Controller for SAS. For "
|
||||||
"documentation, please visit https://docs.datacontroller.io";
|
"documentation, please visit https://docs.datacontroller.io";
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
/* take template from config table */
|
||||||
|
set work.alertmessage;
|
||||||
|
cnt=countw(var_value,'0A'x);
|
||||||
|
do i=1 to cnt;
|
||||||
|
txt=resolve(scan(var_value,i,'0A'x));
|
||||||
|
put txt /;
|
||||||
|
end;
|
||||||
|
%end;
|
||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
%else %if &alert_event=APPROVED %then %do;
|
%else %if &alert_event=APPROVED %then %do;
|
||||||
/* there is no approval message */
|
/* there is no approval message */
|
||||||
data _null_;
|
data _null_;
|
||||||
File __out lrecl=32000;
|
File __out lrecl=32000;
|
||||||
|
length txt $2048;
|
||||||
|
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||||
|
/* fallback message */
|
||||||
put 'Dear user,';
|
put 'Dear user,';
|
||||||
put ' ';
|
put ' ';
|
||||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||||
"been approved by &from_user on the '&syshostname' SAS server.";
|
"been approved by &from_user on the &syshostname SAS server.";
|
||||||
put " ";
|
put " ";
|
||||||
put "This is an automated email by Data Controller for SAS. For "
|
put "This is an automated email by Data Controller for SAS. For "
|
||||||
"documentation, please visit https://docs.datacontroller.io";
|
"documentation, please visit https://docs.datacontroller.io";
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
/* take template from config table */
|
||||||
|
set work.alertmessage;
|
||||||
|
cnt=countw(var_value,'0A'x);
|
||||||
|
do i=1 to cnt;
|
||||||
|
txt=resolve(scan(var_value,i,'0A'x));
|
||||||
|
put txt /;
|
||||||
|
end;
|
||||||
|
%end;
|
||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
%else %if &alert_event=REJECTED %then %do;
|
%else %if &alert_event=REJECTED %then %do;
|
||||||
@@ -170,17 +199,29 @@ filename __out email (&emails)
|
|||||||
run;
|
run;
|
||||||
data _null_;
|
data _null_;
|
||||||
File __out lrecl=32000;
|
File __out lrecl=32000;
|
||||||
|
length txt $2048;
|
||||||
|
%if %mf_getattrn(alertmessage,NLOBS)=0 %then %do;
|
||||||
|
/* fallback message */
|
||||||
put 'Dear user,';
|
put 'Dear user,';
|
||||||
put ' ';
|
put ' ';
|
||||||
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
put "Please be advised that a change to table &alert_lib..&alert_ds has "
|
||||||
"been rejected by &from_user on the '&syshostname' SAS server.";
|
"been rejected by &from_user on the &syshostname SAS server.";
|
||||||
put " ";
|
put " ";
|
||||||
length txt $2048;
|
|
||||||
txt=symget('REVIEW_REASON_TXT');
|
txt=symget('REVIEW_REASON_TXT');
|
||||||
put "Reason provided: " txt;
|
put "Reason provided: " txt;
|
||||||
put " ";
|
put " ";
|
||||||
put "This is an automated email by Data Controller for SAS. For "
|
put "This is an automated email by Data Controller for SAS. For "
|
||||||
"documentation, please visit https://docs.datacontroller.io";
|
"documentation, please visit https://docs.datacontroller.io";
|
||||||
|
%end;
|
||||||
|
%else %do;
|
||||||
|
/* take template from config table */
|
||||||
|
set work.alertmessage;
|
||||||
|
cnt=countw(var_value,'0A'x);
|
||||||
|
do i=1 to cnt;
|
||||||
|
txt=resolve(scan(var_value,i,'0A'x));
|
||||||
|
put txt /;
|
||||||
|
end;
|
||||||
|
%end;
|
||||||
run;
|
run;
|
||||||
%end;
|
%end;
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,44 @@ insert into &lib..mpe_config set
|
|||||||
,var_value=' '
|
,var_value=' '
|
||||||
,var_active=1
|
,var_active=1
|
||||||
,var_desc='Activation Key';
|
,var_desc='Activation Key';
|
||||||
|
insert into &lib..mpe_config set
|
||||||
|
tx_from=0
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="SUBMITTED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been proposed by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||||
|
!!'%superq(SUBMITTED_TXT)'
|
||||||
|
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||||
|
!!'documentation, please visit https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after submitting a change';
|
||||||
|
insert into &lib..mpe_config set
|
||||||
|
tx_from=0
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="APPROVED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been approved by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'This is an automated email by Data'
|
||||||
|
!!' Controller for SAS. For documentation, please visit '
|
||||||
|
!!'https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after approving a change';
|
||||||
|
insert into &lib..mpe_config set
|
||||||
|
tx_from=0
|
||||||
|
,tx_to='31DEC9999:23:59:59'dt
|
||||||
|
,var_scope="DC_EMAIL"
|
||||||
|
,var_name="REJECTED_TEMPLATE"
|
||||||
|
,var_value='Dear user,'!!'0A'x!!'Please be advised that a change to table'
|
||||||
|
!!' &alert_lib..&alert_ds has been rejected by &from_user on the '
|
||||||
|
!!'&syshostname SAS server.'!!'0A'x!!'Reason provided: '
|
||||||
|
!!'%superq(REVIEW_REASON_TXT)'
|
||||||
|
!!'0A'x!!'This is an automated email by Data Controller for SAS. For '
|
||||||
|
!!'documentation, please visit https://docs.datacontroller.io'
|
||||||
|
,var_active=1
|
||||||
|
,var_desc='Template email, sent after rejecting a change';
|
||||||
|
|
||||||
|
|
||||||
insert into &lib..mpe_datadictionary set
|
insert into &lib..mpe_datadictionary set
|
||||||
@@ -213,7 +251,6 @@ insert into &lib..mpe_datadictionary set
|
|||||||
,DD_RESPONSIBLE="&sysuserid"
|
,DD_RESPONSIBLE="&sysuserid"
|
||||||
,DD_SENSITIVITY="Low"
|
,DD_SENSITIVITY="Low"
|
||||||
,tx_to='31DEC5999:23:59:59'dt;
|
,tx_to='31DEC5999:23:59:59'dt;
|
||||||
|
|
||||||
insert into &lib..mpe_datadictionary set
|
insert into &lib..mpe_datadictionary set
|
||||||
tx_from=0
|
tx_from=0
|
||||||
,DD_TYPE='TABLE'
|
,DD_TYPE='TABLE'
|
||||||
@@ -224,7 +261,6 @@ insert into &lib..mpe_datadictionary set
|
|||||||
,DD_RESPONSIBLE="&sysuserid"
|
,DD_RESPONSIBLE="&sysuserid"
|
||||||
,DD_SENSITIVITY="Low"
|
,DD_SENSITIVITY="Low"
|
||||||
,tx_to='31DEC5999:23:59:59'dt;
|
,tx_to='31DEC5999:23:59:59'dt;
|
||||||
|
|
||||||
insert into &lib..mpe_datadictionary set
|
insert into &lib..mpe_datadictionary set
|
||||||
tx_from=0
|
tx_from=0
|
||||||
,DD_TYPE='COLUMN'
|
,DD_TYPE='COLUMN'
|
||||||
@@ -235,7 +271,6 @@ insert into &lib..mpe_datadictionary set
|
|||||||
,DD_RESPONSIBLE="&sysuserid"
|
,DD_RESPONSIBLE="&sysuserid"
|
||||||
,DD_SENSITIVITY="Low"
|
,DD_SENSITIVITY="Low"
|
||||||
,tx_to='31DEC5999:23:59:59'dt;
|
,tx_to='31DEC5999:23:59:59'dt;
|
||||||
|
|
||||||
insert into &lib..mpe_datadictionary set
|
insert into &lib..mpe_datadictionary set
|
||||||
tx_from=0
|
tx_from=0
|
||||||
,DD_TYPE='DIRECTORY'
|
,DD_TYPE='DIRECTORY'
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ data work.reject;
|
|||||||
REVIEW_STATUS_ID="REJECTED";
|
REVIEW_STATUS_ID="REJECTED";
|
||||||
REVIEWED_BY_NM="&user";
|
REVIEWED_BY_NM="&user";
|
||||||
REVIEWED_ON_DTTM=&now;
|
REVIEWED_ON_DTTM=&now;
|
||||||
REVIEW_REASON_TXT=symget('STP_REASON');
|
/* sanitise message to prevent code injection */
|
||||||
|
REVIEW_REASON_TXT=compress(symget('STP_REASON'), '&%;');
|
||||||
run;
|
run;
|
||||||
|
|
||||||
%mp_lockanytable(LOCK,
|
%mp_lockanytable(LOCK,
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
data _null_;
|
data _null_;
|
||||||
set work.sascontroltable;
|
set work.sascontroltable;
|
||||||
call symputx('action',action);
|
call symputx('action',action);
|
||||||
|
/* sanitise message to prevent code injection */
|
||||||
|
message=compress(message, '&%;');
|
||||||
call symputx('message',message);
|
call symputx('message',message);
|
||||||
libds=upcase(libds);
|
libds=upcase(libds);
|
||||||
call symputx('orig_libds',libds);
|
call symputx('orig_libds',libds);
|
||||||
|
|||||||
Reference in New Issue
Block a user