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.
138 lines
3.6 KiB
HTML
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>
|