Files
dc/client/src/app/deploy/sections/dc-location/dc-location.component.html
T
dc 7d2cc846ff
Build / Build-and-ng-test (pull_request) Successful in 6m39s
Lighthouse Checks / lighthouse (pull_request) Successful in 19m8s
Build / Build-and-test-development (pull_request) Successful in 35m11s
feat(deploy): pick and validate the DC location from the deploy screens
The deploy screens asked for a physical folder the deployment user can write
to, as a single text box, and found out whether it worked only by running the
whole deployment.  They now get a picker and a Validate button, backed by the
makedata actions that already answer those two questions.

A new app-dc-location component renders the path as an editable input - the
picker is a convenience over it, never a replacement, because a folder the
deployment user cannot open is not listed and a customer may want a path that
does not exist yet.  Browse lists one level at a time (ACTION=DIRLIST, DEPTH 1,
MAXOBS 500), with Up, a "Use this folder" for where you are standing, and a per
row Use.  Validate calls ACTION=WRITECHECK and reports the verdict inline -
writeable, or the reason it is not - instead of leaving it to the deployment to
fail.

Both screens use it: the Viya setup screen (which runs makedata through the
adapter) and the Sasjs configurator (which opens makedata as a URL).  The
mkdata calls now send ACTION=MAKEDATA, since the service requires it.

Details worth knowing:

- every value read from the service is trimmed: SAS has no zero length
  character value, so a blank field arrives as a single space and reads as a
  value to any `||` or `@if`.
- the listing's level 0 row is the folder being listed, not a folder to drill
  into - except when the parent is blank, where the level 0 rows are the
  platform roots.
- failures are shown inline rather than as abort modals: the two requests pass
  suppressErrorAbortModal/suppressSuccessAbortModal, because a refused path is
  an expected answer here, not a crash.
- parentOf handles a drive root (C:\) and a unix root without inventing an
  empty path.

Frontend tests: 14 new specs (38 in the deploy folder, all passing), including
a template spec for the browser panel and one for the verdict line, both
mutation checked.
2026-11-02 22:32:27 +00:00

138 lines
3.6 KiB
HTML

<div class="dc-location">
<div class="clr-input-wrapper dc-location-input">
<input
clrInput
[id]="inputId"
[name]="inputId"
[ngModel]="path"
(ngModelChange)="onPathChange($event)"
placeholder="Type a folder, or browse below"
/>
<button
type="button"
id="DCBrowse"
class="btn btn-sm btn-outline dc-location-button"
(click)="toggleBrowse()"
>
{{ browsing ? 'Hide' : 'Browse' }}
</button>
<button
type="button"
id="DCValidate"
class="btn btn-sm btn-outline dc-location-button"
[disabled]="validating"
(click)="validate()"
>
@if (validating) {
<clr-spinner clrInline class="spinner-sm"></clr-spinner>
}
@if (!validating) {
Validate
}
</button>
</div>
@if (checkResult) {
<p
id="DCValidateResult"
class="dc-location-result"
[class.dc-location-ok]="checkResult.writeable"
[class.dc-location-bad]="!checkResult.writeable"
>
@if (checkResult.writeable) {
<clr-icon shape="check"></clr-icon>
<span>{{ checkResult.path }} is writeable</span>
}
@if (!checkResult.writeable) {
<clr-icon shape="exclamation-triangle"></clr-icon>
<span>{{ checkResult.msg }}</span>
}
</p>
}
@if (browsing) {
<div class="dc-browser" id="DCBrowser">
<div class="dc-browser-bar">
<button
type="button"
id="DCUp"
class="btn btn-sm btn-link dc-browser-up"
[disabled]="listingIsRoot"
(click)="up()"
>
Up
</button>
<span class="dc-browser-path">
@if (listingIsRoot) {
Select a drive or root folder
}
@if (!listingIsRoot) {
{{ listingParent }}
}
</span>
@if (!listingIsRoot) {
<button
type="button"
id="DCUseCurrent"
class="btn btn-sm btn-outline dc-browser-use"
(click)="useCurrent()"
>
Use this folder
</button>
}
</div>
@if (loading) {
<div class="dc-browser-loading">
<span class="spinner spinner-sm"> Loading... </span>
</div>
}
@if (!loading && !!listingError) {
<p class="dc-location-bad" id="DCBrowserError">
<clr-icon shape="exclamation-triangle"></clr-icon>
<span>{{ listingError }}</span>
</p>
}
@if (!loading && !listingError) {
<ul class="dc-folder-list">
@for (folder of folders; track folder.filepath) {
<li class="dc-folder">
<button
type="button"
class="btn btn-sm btn-link dc-folder-open"
(click)="enter(folder)"
>
<clr-icon shape="folder"></clr-icon>
<span>{{ folder.filename }}</span>
</button>
@if (!!folder.info) {
<span class="dc-folder-info">{{ folder.info }}</span>
}
<button
type="button"
class="btn btn-sm btn-link dc-folder-use"
(click)="choose(folder)"
>
Use
</button>
</li>
}
@if (!folders.length) {
<li class="dc-folder dc-folder-empty" id="DCNoFolders">
No folders here
</li>
}
</ul>
}
@if (truncated) {
<p class="dc-browser-truncated" id="DCTruncated">
Only the first folders are shown - type the path if it is not listed.
</p>
}
</div>
}
</div>