├── sasjs ├── macros │ └── .gitkeep ├── doxy │ ├── logo.png │ ├── favicon.ico │ ├── new_stylesheet.css │ ├── new_footer.html │ ├── Doxyfile │ ├── new_header.html │ └── DoxygenLayout.xml ├── utils │ ├── copysas9.sh │ ├── copyviya.sh │ └── sas9deploy.sh ├── services │ ├── common │ │ ├── getjobcontents.sas │ │ ├── appinit.sas │ │ └── getfoldercontents.sas │ └── edit │ │ └── postjobcontents.sas └── sasjsconfig.json ├── src ├── assets │ ├── .gitkeep │ ├── logo-white.png │ └── angular-logo.png ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── app │ ├── app.component.scss │ ├── home-page │ │ ├── home-page.component.scss │ │ ├── home-page.component.ts │ │ ├── home-page.component.spec.ts │ │ └── home-page.component.html │ ├── components │ │ ├── login-modal │ │ │ ├── login-modal.component.scss │ │ │ ├── login-modal.component.html │ │ │ ├── login-modal.component.spec.ts │ │ │ └── login-modal.component.ts │ │ └── requests-modal │ │ │ ├── requests-modal.component.spec.ts │ │ │ ├── requests-modal.component.scss │ │ │ ├── requests-modal.component.ts │ │ │ └── requests-modal.component.html │ ├── sas.service.spec.ts │ ├── state.service.spec.ts │ ├── app-routing.module.ts │ ├── explorer │ │ ├── explorer.component.spec.ts │ │ ├── explorer.component.scss │ │ ├── explorer.component.html │ │ └── explorer.component.ts │ ├── state.service.ts │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── sas.service.ts │ └── app.component.html ├── main.ts ├── index.html ├── test.ts ├── styles.scss └── polyfills.ts ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc ├── .gitpod.yml ├── CONTRIBUTING.md ├── .gitpod.dockerfile ├── e2e ├── tsconfig.json ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts └── protractor.conf.js ├── tsconfig.app.json ├── tsconfig.spec.json ├── .sasjslint ├── browserslist ├── tsconfig.json ├── .github └── workflows │ └── build.yml ├── karma.conf.js ├── .all-contributorsrc ├── tslint.json ├── package.json ├── README.md └── angular.json /sasjs/macros/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | sasjsbuild/ 4 | .sasjsrc 5 | *.env* -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/folder-navigator/master/src/favicon.ico -------------------------------------------------------------------------------- /sasjs/doxy/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/folder-navigator/master/sasjs/doxy/logo.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /sasjs/doxy/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/folder-navigator/master/sasjs/doxy/favicon.ico -------------------------------------------------------------------------------- /sasjs/doxy/new_stylesheet.css: -------------------------------------------------------------------------------- 1 | #projectlogo img { 2 | border: 0px none; 3 | max-height: 70px; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/folder-navigator/master/src/assets/logo-white.png -------------------------------------------------------------------------------- /src/assets/angular-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sasjs/folder-navigator/master/src/assets/angular-logo.png -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .branding { 2 | min-width: auto !important; 3 | 4 | img { 5 | height: 100%; 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "sasjs.sasjs-for-vscode", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } -------------------------------------------------------------------------------- /sasjs/utils/copysas9.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Used to copy the 'one line deploy' script to the repo root 4 | 5 | cp sasjsbuild/buildsas9.sas buildsas9.txt -------------------------------------------------------------------------------- /sasjs/utils/copyviya.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Used to copy the 'one line deploy' script to the repo root 4 | 5 | cp sasjsbuild/buildviya.sas buildviya.txt -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm i -g @sasjs/cli && npm i 3 | 4 | image: 5 | file: .gitpod.dockerfile 6 | vscode: 7 | extensions: 8 | - sasjs.sasjs-for-vscode 9 | - esbenp.prettier-vscode 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | ### Code Style 3 | 4 | This project uses Prettier to format code. 5 | Please install the 'Prettier - Code formatter' extension for VS Code. 6 | 7 | Files you are editing will automatically be formatted on save. -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.scss: -------------------------------------------------------------------------------- 1 | .home-page { 2 | padding: 16px; 3 | height: calc(100vh - 97px); 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | } 9 | -------------------------------------------------------------------------------- /.gitpod.dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN sudo apt-get update \ 4 | && sudo apt-get install -y \ 5 | doxygen \ 6 | && npm i -g npm@latest \ 7 | && npm i -g @sasjs/cli \ 8 | && npm i \ 9 | && sudo rm -rf /var/lib/apt/lists/* 10 | -------------------------------------------------------------------------------- /sasjs/utils/sas9deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # copy the build file to the SAS server where it can be %inc'd as part of an STP 4 | 5 | rsync -avhe ssh sasjsbuild/sas9.sas --delete mihmed@sas.analytium.co.uk:/tmp 6 | 7 | echo "Now run: %inc '/tmp/sas9.sas';" -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": true, 5 | "editor.formatOnSave": true, 6 | "editor.rulers": [ 7 | 80 8 | ], 9 | "files.trimTrailingWhitespace": true 10 | } -------------------------------------------------------------------------------- /src/app/components/login-modal/login-modal.component.scss: -------------------------------------------------------------------------------- 1 | .modal-body { 2 | ::ng-deep { 3 | .clr-control-container { 4 | width: 100%; 5 | 6 | input { 7 | width: 100%; 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home-page', 5 | templateUrl: './home-page.component.html', 6 | styleUrls: ['./home-page.component.scss'] 7 | }) 8 | export class HomePageComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.sasjslint: -------------------------------------------------------------------------------- 1 | { 2 | "noTrailingSpaces": true, 3 | "noEncodedPasswords": true, 4 | "hasDoxygenHeader": true, 5 | "hasMacroNameInMend": false, 6 | "hasMacroParentheses": true, 7 | "noNestedMacros": false, 8 | "noSpacesInFileNames": true, 9 | "maxLineLength": 120, 10 | "lowerCaseFileNames": true, 11 | "noTabIndentation": true, 12 | "indentationMultiple": 2 13 | } -------------------------------------------------------------------------------- /src/app/sas.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SasService } from './sas.service'; 4 | 5 | describe('SasService', () => { 6 | let service: SasService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(SasService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/state.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { StateService } from './state.service'; 4 | 5 | describe('StateService', () => { 6 | let service: StateService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(StateService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | import { environment } from './environments/environment'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { HomePageComponent } from './home-page/home-page.component'; 4 | import { ExplorerComponent } from './explorer/explorer.component'; 5 | 6 | const routes: Routes = [ 7 | {path: '', redirectTo: 'homepage', pathMatch: 'full'}, 8 | {path: 'homepage', component: HomePageComponent}, 9 | {path: 'explorer', component: ExplorerComponent} 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forRoot(routes, {useHash: true})], 14 | exports: [RouterModule] 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/components/login-modal/login-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "baseUrl": "", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "strict": true, 15 | "target": "ES2015", 16 | "allowJs": true, 17 | "typeRoots": ["node_modules/@types"], 18 | "lib": ["es2018", "dom"] 19 | }, 20 | "angularCompilerOptions": { 21 | "fullTemplateTypeCheck": true, 22 | "strictInjectionParameters": true, 23 | "enablyIvy": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('sas-folder-tree app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/explorer/explorer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ExplorerComponent } from './explorer.component'; 4 | 5 | describe('ExplorerComponent', () => { 6 | let component: ExplorerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ExplorerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ExplorerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomePageComponent } from './home-page.component'; 4 | 5 | describe('HomePageComponent', () => { 6 | let component: HomePageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomePageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomePageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/state.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | export interface AppState { 5 | isUserLoggedIn: boolean 6 | startupData: any 7 | } 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class StateService { 13 | private isUserLoggedIn$ = new BehaviorSubject(true); 14 | public isUserLoggedIn = this.isUserLoggedIn$.asObservable(); 15 | 16 | private startupData$ = new BehaviorSubject(null); 17 | public startupData = this.startupData$.asObservable(); 18 | 19 | public username = new BehaviorSubject(""); 20 | 21 | public setIsLoggedIn(value: boolean) { 22 | this.isUserLoggedIn$.next(value); 23 | } 24 | 25 | public setStartupData(data: any) { 26 | this.startupData$.next(data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/login-modal/login-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginModalComponent } from './login-modal.component'; 4 | 5 | describe('LoginModalComponent', () => { 6 | let component: LoginModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LoginModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/requests-modal/requests-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RequestsModalComponent } from './requests-modal.component'; 4 | 5 | describe('RequestsModalComponent', () => { 6 | let component: RequestsModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RequestsModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RequestsModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SASjs Folder Navigator 6 | 7 | 8 | 9 | 10 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import './polyfills.ts'; 2 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 3 | 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: SASjs Utils Build 5 | 6 | on: 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: Install Dependencies 24 | run: npm ci 25 | - name: Install the SASjs CLI 26 | run: npm i -g @sasjs/cli 27 | - name: Check that the project actually builds 28 | run: sasjs cb -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /src/app/components/login-modal/login-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { SasService } from '../../sas.service'; 3 | 4 | @Component({ 5 | selector: 'app-login-modal', 6 | templateUrl: './login-modal.component.html', 7 | styleUrls: ['./login-modal.component.scss'] 8 | }) 9 | export class LoginModalComponent implements OnInit { 10 | userName = ''; 11 | password = ''; 12 | 13 | loginLoading: boolean = false 14 | 15 | constructor( 16 | public sasService: SasService 17 | ) {} 18 | 19 | ngOnInit() { 20 | 21 | } 22 | 23 | signIn() { 24 | this.loginLoading = true 25 | 26 | this.sasService.login(this.userName, this.password).then((success: any) => { 27 | this.loginLoading = false 28 | 29 | if (success) { 30 | } else { 31 | alert("Wrong username or password, please try again."); 32 | } 33 | }, (err: any) => { 34 | this.loginLoading = false 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sasjs/doxy/new_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: Roboto, 'Helvetica Neue', sans-serif; 10 | } 11 | 12 | .code { 13 | font-family: Monaco, Courier, monospace; 14 | border: 1px solid #d9d9d9; 15 | padding: 5px; 16 | border-radius: 3px; 17 | background-color: #4a3f3f; 18 | color: #f79205; 19 | } 20 | 21 | .menu-divider { 22 | margin: 0 10px !important; 23 | } 24 | 25 | .data-page { 26 | width: 100%; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | padding-top: 20px; 31 | 32 | .areas-select-wrapper { 33 | display: flex; 34 | align-items: center; 35 | 36 | ::ng-deep clr-select-container { 37 | margin: 0; 38 | 39 | select { 40 | min-width: 200px; 41 | } 42 | } 43 | 44 | button { 45 | margin-left: 10px; 46 | } 47 | } 48 | 49 | table { 50 | width: 80%; 51 | margin-top: 40px; 52 | } 53 | } 54 | 55 | .pointer { 56 | cursor: pointer; 57 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/sas-folder-tree'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'sas-folder-tree'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | }); 27 | 28 | it('should render title', () => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | fixture.detectChanges(); 31 | const compiled = fixture.nativeElement; 32 | expect(compiled.querySelector('.content span').textContent).toContain('sas-folder-tree app is running!'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "folder-navigator", 3 | "projectOwner": "sasjs", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "medjedovicm", 15 | "name": "Mihajlo Medjedovic", 16 | "avatar_url": "https://avatars.githubusercontent.com/u/18329105?v=4", 17 | "profile": "https://github.com/medjedovicm", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "allanbowe", 24 | "name": "Allan Bowe", 25 | "avatar_url": "https://avatars.githubusercontent.com/u/4420615?v=4", 26 | "profile": "https://github.com/allanbowe", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "shivakrishnay", 33 | "name": "shivakrishnay", 34 | "avatar_url": "https://avatars.githubusercontent.com/u/88433957?v=4", 35 | "profile": "https://github.com/shivakrishnay", 36 | "contributions": [ 37 | "bug" 38 | ] 39 | } 40 | ], 41 | "contributorsPerLine": 7 42 | } 43 | -------------------------------------------------------------------------------- /sasjs/services/common/getjobcontents.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief Get job contents 4 | @details Will fetch the contents (code) of a viya job or SAS 9 stored process. 5 | 6 |

Service Inputs

7 |
INDATA
8 | |folderpath|jobname| 9 | |---|---| 10 | |/some/folder|someJob| 11 | 12 |

Service Outputs

13 |
CODECONTENT
14 | |codeline| 15 | |---| 16 | |data demo;| 17 | | put 'this is some SAS code';| 18 | |run;| 19 | 20 | 21 |

SAS Macros

22 | @li mf_getplatform.sas 23 | @li mv_getjobcode.sas 24 | @li mv_getjobcode.sas 25 | @li mm_getstpcode.sas 26 | 27 | **/ 28 | 29 | %webout(FETCH) 30 | 31 | data _null_; 32 | set indata; 33 | call symputx('folderpath',folderpath); 34 | call symputx('jobname',jobname); 35 | run; 36 | 37 | %let codepath=%sysfunc(pathname(work))/code.sas; 38 | 39 | %macro getcontent(); 40 | %if %mf_getplatform()=SASVIYA %then %do; 41 | %mv_getjobcode( 42 | path=&folderpath 43 | ,name=&jobname 44 | ,outfile=&codepath 45 | ) 46 | %end; 47 | %else %do; 48 | %mm_getstpcode(tree=&folderpath 49 | ,name=&jobname 50 | ,outloc=&codepath 51 | ) 52 | %end; 53 | %mend; 54 | %getcontent() 55 | 56 | data work.codecontent; 57 | infile "&codepath"; 58 | input; 59 | codeline=_infile_; 60 | run; 61 | 62 | %webout(OPEN) 63 | %webout(OBJ,codecontent) 64 | %webout(CLOSE) 65 | -------------------------------------------------------------------------------- /src/app/home-page/home-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to the SASjs SAS folder navigator!

3 | 9 |
10 |
11 | SASjs Adapter: 12 | 13 | https://github.com/sasjs/adapter 14 | 15 |
16 |
17 | 23 |
24 |
25 | SASjs Macro Core: 26 | 27 | https://github.com/sasjs/core 28 | 29 |
30 |
31 | 37 |
-------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { StateService } from './state.service'; 3 | import { SasService } from './sas.service'; 4 | 5 | import { SASjsConfig } from '@sasjs/adapter'; 6 | 7 | @Component({ 8 | selector: 'app-root', 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.scss'] 11 | }) 12 | export class AppComponent implements OnInit { 13 | public isLoggedIn: boolean = true; 14 | public requestModal: boolean = false; 15 | public sasjsConfig: SASjsConfig = new SASjsConfig(); 16 | public username: string = ''; 17 | 18 | constructor(private stateService: StateService, private sasService: SasService) { 19 | sasService.fetchStartupData(); 20 | } 21 | 22 | ngOnInit() { 23 | this.getSasjsConfig(); 24 | 25 | this.stateService.isUserLoggedIn.subscribe((isLoggedIn: boolean) => { 26 | this.isLoggedIn = isLoggedIn; 27 | }); 28 | 29 | this.stateService.username.subscribe((username: string) => { 30 | this.username = username; 31 | }); 32 | } 33 | 34 | public debugChanged() { 35 | if (this.sasjsConfig) { 36 | this.sasService.setDebugState(this.sasjsConfig.debug); 37 | } 38 | } 39 | 40 | public getSasjsConfig() { 41 | this.sasjsConfig = this.sasService.getSasjsConfig(); 42 | } 43 | 44 | public logout() { 45 | this.sasService.logout(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /sasjs/services/common/appinit.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file appinit.sas 3 | @brief Initialisation service - runs on app startup 4 | @details This is always the first service called when the app is opened. 5 | 6 |

SAS Macros

7 | @li mf_getplatform.sas 8 | @li mv_getfoldermembers.sas 9 | @li mm_getfoldermembers.sas 10 | 11 |

Service Outputs

12 |
FOLDERS
13 | |itemid $|itemname $| itemtype $|itempath $| 14 | |---|---|---|---| 15 | |ff726405-3548-40b0-acd3-99589ec0d112|Public|Folder|/| 16 | |c63c5bcc-7b34-4f37-9558-8cb1c0824d23|Users|Folder|/| 17 | 18 | 19 | **/ 20 | 21 | %macro appinit(); 22 | %if %mf_getplatform()=SASVIYA %then %do; 23 | %mv_getfoldermembers(root=/,outds=folders) 24 | data folders; 25 | length itemtype $32; 26 | set folders(rename=(name=itemname id=itemid type=itemtype)); 27 | keep itemname itemid itemtype itempath; 28 | if itemtype in ('folder','userRoot') then itemtype='Folder'; 29 | itempath="/"; 30 | run; 31 | %end; 32 | %else %do; 33 | %mm_getfoldermembers(root=/,outds=folders) 34 | data folders; 35 | set folders(rename=(metauri=itemid metaname=itemname metatype=itemtype)); 36 | keep itemname itemid itemtype itempath; 37 | itempath='/'; 38 | run; 39 | proc sort; 40 | by itemname; 41 | run; 42 | %end; 43 | %mend; 44 | 45 | %appinit() 46 | 47 | %webout(OPEN) 48 | %webout(OBJ,folders) 49 | %webout(CLOSE) 50 | -------------------------------------------------------------------------------- /src/app/components/requests-modal/requests-modal.component.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep { 2 | .requests-modal .modal-header .close clr-icon { 3 | display: block !important; 4 | } 5 | 6 | .work-tables-dropdown button { 7 | color: var(--clr-nav-link-color, #8c8c8c) !important; 8 | } 9 | 10 | .stack-view { 11 | height: auto !important; 12 | } 13 | 14 | .content { 15 | clr-icon { 16 | margin-bottom: 5px; 17 | } 18 | 19 | pre { 20 | word-break: break-all; 21 | white-space: pre-wrap; 22 | max-height: initial; 23 | overflow: visible; 24 | border: 0; 25 | } 26 | 27 | .stack-block-label { 28 | width: 100%; 29 | padding-left: .6rem !important; 30 | 31 | .stack-view-key { 32 | display: none !important; 33 | } 34 | } 35 | } 36 | } 37 | 38 | .dropdown-item { 39 | &.selected { 40 | background: #d8e3e9; 41 | } 42 | } 43 | 44 | .log-wrapper { 45 | min-height: 50px; 46 | padding: 10px; 47 | margin-top: 10px; 48 | 49 | white-space: pre-wrap; 50 | border-radius: 3px; 51 | 52 | border: 1px solid #e2e2e2; 53 | background-color: #fbfbfb; 54 | 55 | height: 48vh; 56 | overflow: auto; 57 | } 58 | 59 | .no-reqs { 60 | border-top: 1px solid #0000001a; 61 | padding-top: 5px; 62 | text-align: center; 63 | } -------------------------------------------------------------------------------- /sasjs/doxy/Doxyfile: -------------------------------------------------------------------------------- 1 | ALPHABETICAL_INDEX = NO 2 | 3 | ENABLE_PREPROCESSING = NO 4 | EXTENSION_MAPPING = sas=Java ddl=Java 5 | EXTRACT_LOCAL_CLASSES = NO 6 | FILE_PATTERNS = *.sas \ 7 | *.ddl \ 8 | *.dox 9 | GENERATE_LATEX = NO 10 | GENERATE_TREEVIEW = YES 11 | HIDE_FRIEND_COMPOUNDS = YES 12 | HIDE_IN_BODY_DOCS = YES 13 | HIDE_SCOPE_NAMES = YES 14 | HIDE_UNDOC_CLASSES = YES 15 | HIDE_UNDOC_MEMBERS = YES 16 | HTML_OUTPUT = $(DOXY_HTML_OUTPUT) 17 | HTML_HEADER = $(DOXY_CONTENT)new_header.html 18 | HTML_EXTRA_FILES = $(DOXY_CONTENT)favicon.ico 19 | HTML_FOOTER = $(DOXY_CONTENT)new_footer.html 20 | HTML_EXTRA_STYLESHEET = $(DOXY_CONTENT)new_stylesheet.css 21 | INHERIT_DOCS = NO 22 | INLINE_INFO = NO 23 | INPUT = $(DOXY_CONTENT)../../README.md $(DOXY_INPUT) 24 | LAYOUT_FILE = $(DOXY_CONTENT)DoxygenLayout.xml 25 | USE_MDFILE_AS_MAINPAGE = README.md 26 | MAX_INITIALIZER_LINES = 0 27 | PROJECT_NAME = $(PROJECT_NAME) 28 | PROJECT_LOGO = $(DOXY_CONTENT)logo.png 29 | PROJECT_BRIEF = $(PROJECT_BRIEF) 30 | RECURSIVE = YES 31 | REPEAT_BRIEF = NO 32 | SHOW_NAMESPACES = NO 33 | SHOW_USED_FILES = NO 34 | SOURCE_BROWSER = YES 35 | SOURCE_TOOLTIPS = NO 36 | STRICT_PROTO_MATCHING = YES 37 | STRIP_CODE_COMMENTS = NO 38 | SUBGROUPING = NO 39 | TAB_SIZE = 2 40 | VERBATIM_HEADERS = NO -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { FormsModule } from '@angular/forms'; 8 | 9 | import { LoginModalComponent } from './components/login-modal/login-modal.component'; 10 | import { HomePageComponent } from './home-page/home-page.component'; 11 | import { ClarityModule } from '@clr/angular'; 12 | import { RequestsModalComponent } from './components/requests-modal/requests-modal.component'; 13 | import { ExplorerComponent } from './explorer/explorer.component'; 14 | import { AceModule } from 'ngx-ace-wrapper'; 15 | import { ACE_CONFIG } from 'ngx-ace-wrapper'; 16 | import { AceConfigInterface } from 'ngx-ace-wrapper'; 17 | 18 | const DEFAULT_ACE_CONFIG: AceConfigInterface = { 19 | fontSize: '17px' 20 | }; 21 | 22 | @NgModule({ 23 | declarations: [AppComponent, LoginModalComponent, HomePageComponent, RequestsModalComponent, ExplorerComponent], 24 | imports: [ 25 | BrowserModule, 26 | AppRoutingModule, 27 | BrowserAnimationsModule, 28 | FormsModule, 29 | ClarityModule, 30 | AceModule 31 | ], 32 | providers: [ 33 | { 34 | provide: ACE_CONFIG, 35 | useValue: DEFAULT_ACE_CONFIG 36 | } 37 | ], 38 | bootstrap: [AppComponent], 39 | entryComponents: [LoginModalComponent] 40 | }) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /sasjs/services/edit/postjobcontents.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief Update job contents 4 | @details Will write back SAS code to a Stored Process or Viya Job 5 | 6 |

Service Inputs

7 |
INDATA
8 | |folderpath $|jobname $| 9 | |---|---| 10 | |/some/folder|someJob| 11 | 12 |
SOURCECODE
13 | |codeline $| 14 | |---| 15 | |proc sort;| 16 | |quit;| 17 | 18 |

Service Outputs

19 |
RESULT
20 | |result $| 21 | |---| 22 | |SUCCESS| 23 | 24 | Alternatively, sasjsabort will be returned. 25 | 26 | 27 |

SAS Macros

28 | @li mf_getplatform.sas 29 | @li mf_nobs.sas 30 | @li mp_abort.sas 31 | @li mv_createjob.sas 32 | @li mm_updatestpsourcecode.sas 33 | 34 | **/ 35 | 36 | %webout(FETCH) 37 | 38 | data _null_; 39 | set indata; 40 | call symputx('folderpath',folderpath); 41 | call symputx('jobname',jobname); 42 | run; 43 | 44 | 45 | %mp_abort(iftrue= (%mf_nobs(work.sourcecode)=0) 46 | ,mac=&_program 47 | ,msg=%str(source table SOURCECODE is empty ) 48 | ) 49 | %mp_abort(iftrue= (&syscc ne 0) 50 | ,mac=&_program 51 | ,msg=%str(syscc=&syscc) 52 | ) 53 | 54 | filename inref temp; 55 | data _null_; 56 | file inref termstr=LF; 57 | set work.sourcecode; 58 | put codeline; 59 | run; 60 | 61 | %macro postcontent(); 62 | %if %mf_getplatform()=SASVIYA %then %do; 63 | %mv_createjob( 64 | path=&folderpath 65 | ,name=&jobname 66 | ,code=inref 67 | ,replace=YES 68 | ) 69 | %end; 70 | %else %do; 71 | %mm_updatestpsourcecode( 72 | stp=&folderpath/&jobname 73 | ,stpcode=inref 74 | ) 75 | %end; 76 | %mend postcontent; 77 | 78 | %postcontent() 79 | 80 | %mp_abort(iftrue= (&syscc ne 0) 81 | ,mac=&_program 82 | ,msg=%str(syscc=&syscc) 83 | ) 84 | 85 | data work.result; 86 | result='SUCCESS'; 87 | run; 88 | 89 | %webout(OPEN) 90 | %webout(OBJ,result) 91 | %webout(CLOSE) 92 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [true, "attribute", "app", "camelCase"], 13 | "component-selector": [true, "element", "app", "kebab-case"], 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "interface-name": false, 16 | "max-classes-per-file": false, 17 | "max-line-length": [true, 140], 18 | "member-access": false, 19 | "member-ordering": [ 20 | true, 21 | { 22 | "order": ["static-field", "instance-field", "static-method", "instance-method"] 23 | } 24 | ], 25 | "no-consecutive-blank-lines": false, 26 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 27 | "no-empty": false, 28 | "no-inferrable-types": [true, "ignore-params"], 29 | "no-non-null-assertion": true, 30 | "no-redundant-jsdoc": true, 31 | "no-switch-case-fall-through": true, 32 | "no-var-requires": false, 33 | "object-literal-key-quotes": [true, "as-needed"], 34 | "object-literal-sort-keys": false, 35 | "ordered-imports": false, 36 | "quotemark": [true, "single"], 37 | "trailing-comma": false, 38 | "no-conflicting-lifecycle": true, 39 | "no-host-metadata-property": true, 40 | "no-input-rename": true, 41 | "no-inputs-metadata-property": true, 42 | "no-output-native": true, 43 | "no-output-on-prefix": true, 44 | "no-output-rename": true, 45 | "no-outputs-metadata-property": true, 46 | "template-banana-in-box": true, 47 | "template-no-negated-async": true, 48 | "use-lifecycle-interface": true, 49 | "use-pipe-transform-interface": true 50 | }, 51 | "rulesDirectory": ["codelyzer"] 52 | } 53 | -------------------------------------------------------------------------------- /sasjs/services/common/getfoldercontents.sas: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @brief Get folder contents 4 | @details Will fetch the contents of a particular folder given a `folderpath` 5 | as input. 6 | 7 |

Service Inputs

8 |
INDATA
9 | |folderpath| 10 | |---| 11 | |/Public/app/foldernavigator/services/common| 12 | 13 |

Service Outputs

14 |
CONTENT
15 | |itemid $|itemname $| itemtype $|itempath $| 16 | |---|---|---|---| 17 | |ff726405-3548-40b0-acd3-99589ec0d112|appinit|jobDefinition|/Public/app/foldernavigator/services/common| 18 | |c63c5bcc-7b34-4f37-9558-8cb1c0824d23|getfoldercontents|jobDefinition|/Public/app/foldernavigator/services/common| 19 | 20 |

SAS Macros

21 | @li mf_getplatform.sas 22 | @li mv_getfoldermembers.sas 23 | @li mm_getfoldermembers.sas 24 | 25 | **/ 26 | 27 | %webout(FETCH) 28 | 29 | data _null_; 30 | set indata; 31 | call symputx('folderpath',folderpath); 32 | run; 33 | 34 | %macro getcontent(); 35 | %if %mf_getplatform()=SASVIYA %then %do; 36 | %mv_getfoldermembers(root=&folderpath,outds=work.folders) 37 | data folders; 38 | length itemtype $32 name id $64; 39 | set folders(rename=(name=itemname id=itemid type=itemtype)); 40 | keep itemname itemid itemtype itempath; 41 | itemname=name; 42 | itemid=id; 43 | itemtype=type; 44 | if itemtype in ('folder','userRoot') then itemtype='Folder'; 45 | else if cats(contenttype)='folder' then itemtype='Folder'; 46 | else itemtype=cats(contenttype); 47 | itempath="&folderpath"; 48 | run; 49 | %end; 50 | %else %do; 51 | %mm_getfoldermembers(root=&folderpath,outds=folders) 52 | data folders; 53 | set folders(rename=(metauri=itemid metaname=itemname metatype=itemtype)); 54 | keep itemname itemid itemtype itempath; 55 | itempath="&folderpath"; 56 | run; 57 | %end; 58 | %mend; 59 | 60 | %getcontent() 61 | 62 | proc sort; 63 | by itemname; 64 | run; 65 | 66 | %webout(OPEN) 67 | %webout(OBJ,folders) 68 | %webout(CLOSE) 69 | -------------------------------------------------------------------------------- /sasjs/sasjsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cli.sasjs.io/sasjsconfig-schema.json", 3 | "macroFolders": [ 4 | "sasjs/macros" 5 | ], 6 | "serviceConfig": { 7 | "serviceFolders": [ 8 | "sasjs/services/common", 9 | "sasjs/services/edit" 10 | ] 11 | }, 12 | "defaultTarget": "viya", 13 | "targets": [ 14 | { 15 | "name": "viya", 16 | "serverUrl": "https://sas.analytium.co.uk", 17 | "serverType": "SASVIYA", 18 | "allowInsecureRequests": false, 19 | "appLoc": "/Public/app/folder-navigator", 20 | "macroFolders": [], 21 | "programFolders": [], 22 | "buildConfig": { 23 | "buildOutputFileName": "buildviya.sas", 24 | "buildResultsFolder": "sasjsresults", 25 | "buildOutputFolder": "sasjsbuild", 26 | "initProgram": "", 27 | "termProgram": "", 28 | "macroVars": {} 29 | }, 30 | "streamConfig": { 31 | "assetPaths": [], 32 | "streamWeb": true, 33 | "streamWebFolder": "web", 34 | "webSourcePath": "dist", 35 | "streamServiceName": "clickme" 36 | }, 37 | "deployConfig": { 38 | "deployServicePack": true, 39 | "deployScripts": [ 40 | "sasjs/utils/copyviya.sh" 41 | ] 42 | }, 43 | "contextName": "SAS Job Execution compute context" 44 | }, 45 | { 46 | "name": "sas9", 47 | "serverType": "SAS9", 48 | "appLoc": "/User Folders/&sysuserid/folder-navigator", 49 | "deployConfig": { 50 | "deployScripts": [ 51 | "sasjs/utils/copysas9.sh", 52 | "sasjs/utils/sas9deploy.sh" 53 | ] 54 | }, 55 | "buildConfig": { 56 | "buildOutputFileName": "buildsas9.sas" 57 | }, 58 | "serverName": "SASApp", 59 | "repositoryName": "Foundation", 60 | "streamConfig": { 61 | "assetPaths": [ 62 | "./assets" 63 | ], 64 | "streamWeb": true, 65 | "streamWebFolder": "web", 66 | "webSourcePath": "dist", 67 | "streamServiceName": "clickme" 68 | } 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "folder-navigator", 3 | "description": "A SASjs web app for navigating folder in SAS 9 and Viya and modifying Stored Process / Job content.", 4 | "version": "2.0.0", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build --prod --aot", 9 | "demobuild": "npm run build && sasjs cb -t viya && mv sasjsbuild/buildviya.sas buildviya.txt && sasjs cb -t sas9 && mv sasjsbuild/buildsas9.sas buildsas9.txt", 10 | "test": "ng test", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e", 13 | "build-watch": "ng build --watch", 14 | "deploy": "rsync -avhe ssh ./dist/* --delete $SSH_ACCOUNT:$DEPLOY_PATH", 15 | "sync": "./node_modules/.bin/watch --wait=3 \"echo Account: $SSH_ACCOUNT && npm run deploy && echo 'App is synced!'\" dist" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "~9.0.7", 20 | "@angular/cdk": "^9.2.4", 21 | "@angular/common": "~9.0.7", 22 | "@angular/compiler": "~9.0.7", 23 | "@angular/core": "~9.0.7", 24 | "@angular/forms": "~9.0.7", 25 | "@angular/platform-browser": "~9.0.7", 26 | "@angular/platform-browser-dynamic": "~9.0.7", 27 | "@angular/router": "~9.0.7", 28 | "@clr/angular": "^3.1.2", 29 | "@clr/core": "3.1.15", 30 | "@clr/icons": "5.1.0", 31 | "@clr/ui": "5.1.0", 32 | "@sasjs/adapter": "^2.10.5", 33 | "@sasjs/utils": "^2.28.0", 34 | "@webcomponents/webcomponentsjs": "^2.5.0", 35 | "lodash-es": "^4.17.21", 36 | "moment": "^2.29.1", 37 | "ngx-ace-wrapper": "^11.0.0", 38 | "rxjs": "~6.6.3", 39 | "tslib": "^1.14.1", 40 | "zone.js": "~0.11.4" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "~0.901.15", 44 | "@angular/cli": "~9.0.7", 45 | "@angular/compiler-cli": "~9.0.7", 46 | "@angular/language-service": "~11.2.4", 47 | "@sasjs/cli": "^2.37.8", 48 | "@sasjs/core": "^2.45.2", 49 | "@types/jasmine": "~3.6.4", 50 | "@types/jasminewd2": "~2.0.3", 51 | "@types/lodash-es": "^4.17.4", 52 | "@types/node": "^14.14.31", 53 | "codelyzer": "^6.0.1", 54 | "ghooks": "^2.0.4", 55 | "jasmine-core": "~3.6.0", 56 | "jasmine-spec-reporter": "~6.0.0", 57 | "karma": "~6.1.1", 58 | "karma-chrome-launcher": "~3.1.0", 59 | "karma-coverage-istanbul-reporter": "~3.0.3", 60 | "karma-jasmine": "~4.0.1", 61 | "karma-jasmine-html-reporter": "^1.5.4", 62 | "prettier": "^2.2.1", 63 | "protractor": "~7.0.0", 64 | "ts-node": "~9.1.1", 65 | "tslint": "~6.1.3", 66 | "typescript": "~3.7.5", 67 | "watch": "^1.0.2" 68 | }, 69 | "config": { 70 | "ghooks": { 71 | "pre-commit": "sasjs lint" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /sasjs/doxy/new_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | $projectname: $title 11 | 12 | 13 | 14 | $title 15 | 16 | 17 | 18 | 19 | $treeview $search $mathjax 20 | 21 | 22 | $extrastylesheet 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
42 |
43 |  $projectnumber 46 |
47 | 48 |
$projectbrief
49 | 50 |
$searchbox
60 |
61 | 62 | 63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/app/sas.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import SASjs from '@sasjs/adapter'; 4 | import { StateService } from './state.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class SasService { 10 | private _sasService: any; 11 | 12 | constructor(private stateService: StateService) { 13 | const adapterSettings: string | null = JSON.parse( 14 | localStorage.getItem('adapterSettings') || '{}' 15 | ) 16 | 17 | this._sasService = new SASjs(adapterSettings); 18 | } 19 | 20 | public fetchStartupData() { 21 | this.request('common/appinit', null).then((response: any) => { 22 | this.stateService.setStartupData(response.folders); 23 | }); 24 | } 25 | 26 | public request(url: string, data: any, config?: any) { 27 | url = "services/" + url 28 | 29 | return new Promise((resolve, reject) => { 30 | this._sasService 31 | .request(url, data, config, (loginRequired: boolean) => { 32 | this.stateService.setIsLoggedIn(false); 33 | }) 34 | .then( 35 | (res: any) => { 36 | if (res.login === false) { 37 | this.stateService.setIsLoggedIn(false); 38 | this.stateService.username.next(''); 39 | reject(false); 40 | } 41 | 42 | if (this.stateService.username.getValue().length < 1 && res.MF_GETUSER) { 43 | this.stateService.username.next(res.MF_GETUSER); 44 | } 45 | 46 | if (res.status === 404) { 47 | reject({ MESSAGE: res.body || 'SAS responded with an error' }); 48 | } 49 | 50 | resolve(res); 51 | }, 52 | (err: any) => { 53 | reject(err); 54 | } 55 | ); 56 | }); 57 | } 58 | 59 | public async login(username: string, password: string) { 60 | return this._sasService 61 | .logIn(username, password) 62 | .then( 63 | (res: { isLoggedIn: boolean; userName: string }) => { 64 | this.stateService.setIsLoggedIn(res.isLoggedIn); 65 | 66 | this.stateService.username.next(res.userName); 67 | 68 | return res.isLoggedIn; 69 | }, 70 | (err: any) => { 71 | console.error(err); 72 | this.stateService.setIsLoggedIn(false); 73 | return false; 74 | } 75 | ) 76 | .catch((e: any) => { 77 | if (e === 403) { 78 | console.error('Invalid host'); 79 | } 80 | return false; 81 | }); 82 | } 83 | 84 | public logout() { 85 | this._sasService.logOut().then(() => { 86 | this.stateService.setIsLoggedIn(false); 87 | this.stateService.username.next(''); 88 | }); 89 | } 90 | 91 | public getSasjsConfig() { 92 | return this._sasService.getSasjsConfig(); 93 | } 94 | 95 | public getSasRequests() { 96 | return this._sasService.getSasRequests(); 97 | } 98 | 99 | public setDebugState(state: boolean) { 100 | this._sasService.setDebugState(state); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sasjs/doxy/DoxygenLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /src/app/components/requests-modal/requests-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { SasService } from '../../sas.service'; 3 | import * as moment from 'moment'; 4 | 5 | @Component({ 6 | selector: 'app-requests-modal', 7 | templateUrl: './requests-modal.component.html', 8 | styleUrls: ['./requests-modal.component.scss'] 9 | }) 10 | export class RequestsModalComponent implements OnInit { 11 | private _opened: boolean = false; 12 | get opened(): boolean { 13 | return this._opened; 14 | } 15 | @Input() 16 | set opened(value: boolean) { 17 | this._opened = value; 18 | if (value) this.modalOpened(); 19 | } 20 | 21 | @Output() openedChange = new EventEmitter(); 22 | 23 | public sasLogActive: boolean = true; 24 | public sasSourceCodeActive: boolean = false; 25 | public sasGeneratedCodeActive: boolean = false; 26 | public tablesActive: boolean = false; 27 | 28 | public sasjsConfig: any; 29 | public sasjsRequests: any[] = []; 30 | public workTables: any; 31 | 32 | constructor(private sasService: SasService) { 33 | 34 | } 35 | 36 | ngOnInit(): void {} 37 | 38 | public parseLogTimestamp(timestamp: any) { 39 | return `${this.formatTimestamp(timestamp)} ${this.timestampFromNow(timestamp)}` 40 | } 41 | 42 | public formatTimestamp(timestamp: any) { 43 | return moment(timestamp).format 44 | ? moment(timestamp).format('dddd, MMMM Do YYYY, h:mm:ss a') 45 | : timestamp; 46 | } 47 | 48 | public timestampFromNow(timestamp: any) { 49 | return moment(timestamp).format 50 | ? ` (${moment(timestamp).fromNow()})` 51 | : ''; 52 | } 53 | 54 | public modalOpenChange(state: any) { 55 | this.opened = state; 56 | this.openedChange.emit(this.opened); 57 | } 58 | 59 | public modalOpened() { 60 | this.sasjsConfig = this.sasService.getSasjsConfig(); 61 | this.sasjsRequests = this.sasService.getSasRequests(); 62 | 63 | for (let req of this.sasjsRequests) { 64 | this.parseErrorsAndWarnings(req); 65 | 66 | req['appLoc'] = this.cutAppLoc(req.serviceLink); 67 | req['parsedTimestamp'] = this.parseLogTimestamp(req.timestamp); 68 | } 69 | } 70 | 71 | public cutAppLoc(link: string) { 72 | return link.replace(this.sasjsConfig.appLoc + '/', ''); 73 | } 74 | 75 | public goToLogLine(linkingLine: string, requestStackId: string, type: string) { 76 | let allLines: any = document.querySelectorAll(`#${requestStackId} .log-wrapper.saslog font`); 77 | let logWrapper: any = document.querySelector(`#${requestStackId} .log-wrapper.saslog`); 78 | 79 | for (let line of allLines) { 80 | if (line.textContent.includes(linkingLine)) { 81 | logWrapper.scrollTop = line.offsetTop - logWrapper.offsetTop; 82 | line.style.backgroundColor = "#61a2202b"; 83 | 84 | setTimeout(() => { 85 | line.style = ''; 86 | }, 3000); 87 | } 88 | } 89 | } 90 | 91 | public async parseErrorsAndWarnings(req: any) { 92 | if (!req || !req.logFile) return; 93 | if (req['logErrors'] !== undefined || req['logWarnings'] !== undefined) return; 94 | 95 | let errorLines = []; 96 | let warningLines = []; 97 | 98 | let logLines = req.logFile.split('\n'); 99 | 100 | for (let i = 0; i < logLines.length; i++) { 101 | if (/<.*>ERROR/gm.test(logLines[i])) { 102 | let errorLine = logLines[i].substring(logLines[i].indexOf('E'), logLines[i].length - 1); 103 | errorLines.push(errorLine); 104 | } else if (/^ERROR/gm.test(logLines[i])) { 105 | errorLines.push(logLines[i]); 106 | 107 | logLines[i] = '' + logLines[i] + ''; 108 | } 109 | 110 | if (/<.*>WARNING/gm.test(logLines[i])) { 111 | let warningLine = logLines[i].substring(logLines[i].indexOf('W'), logLines[i].length - 1); 112 | warningLines.push(warningLine); 113 | } else if (/^WARNING/gm.test(logLines[i])) { 114 | warningLines.push(logLines[i]); 115 | 116 | logLines[i] = '' + logLines[i] + ''; 117 | } 118 | } 119 | 120 | req.logFile = logLines.join('\n'); 121 | req['logErrors'] = errorLines; 122 | req['logWarnings'] = warningLines; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | The folderNavigator lets you navigate the SAS folder tree - be that metadata in SAS 9 or SAS Drive in Viya. 7 | 8 | The purpose of building the app was to provide a way to modify Stored Process source code for developers on Unix / Mac - who don't have Enterprise Guide nor the patience to [wait for X11](https://rawsas.com/launching-smc-on-mac-os-over-ssh-with-x11/). 9 | 10 | You can deploy the app in just two lines of code: 11 | 12 | ``` 13 | filename runme url "https://raw.githubusercontent.com/sasjs/folder-navigator/master/build.sas"; 14 | %inc runme; 15 | ``` 16 | 17 | To provide the convenience of running those two lines above in both SAS 9 and SAS Viya (without too much rework on our part) the services contain code for both SAS 9 and Viya. This redundancy (deploying Viya code to SAS 9 and vice versa) is not necessary if you are using the [SASjs CLI](https://cli.sasjs.io) - instead you could define your Viya or SAS 9 specific macros in the target-specific `macroFolders` and compile/build/deploy relevant code to each target whilst keeping a common codebase in GIT. 18 | 19 | We do this extensively with [Data Controller](https://datacontroller.io) as well as customer project built with SASjs (where those projects need to work on both SAS 9 and Viya). 20 | 21 | For more information, a demo, or SASjs training, contact [Allan Bowe](https://www.linkedin.com/in/allanbowe). 22 | 23 | ## Full Deployment 24 | 25 | ### Frontend Web 26 | 27 | Clone the repo, `cd` into it, and `npm install`. Then update the following in `sas.service.ts`: 28 | 29 | - `appLoc` - the location in the metadata or viya folder tree where the backend services will be located. 30 | - `serverType` - either SAS9 or SASVIYA. 31 | - `serverUrl` - only relevant if not serving from the SAS domain (`!SASCONFIG/LevX/Web/WebServer/htdocs` in SAS9 or `/var/www/html` on SAS Viya) 32 | - `useComputeApi` - can be `true` or `false`, it's a switch for SASjs adapter whether to use `Compute` approach while doing requests (Viya only). 33 | - `contextName` - only relevant if `useComputeApi` is true. Provides a context name that will be used in adapter. 34 | 35 | More details in official @SASjs/adapter documentation: https://sasjs.io/sasjs-adapter/#configuration 36 | 37 | If you are running locally you will either need to whitelist `localhost` on the server, or enable CORS as described [here](https://sasjs.io/cors) 38 | 39 | Finally, execute `npm run build` to create a production build. 40 | 41 | ### Backend Services 42 | 43 | Simply configure the target in the `sasjsconfig.json` and run `sasjs cbd -t yourTarget` to compile, build & deploy the backend services to your environment. 44 | 45 | ## Contributors ✨ 46 | 47 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |

Mihajlo Medjedovic

💻

Allan Bowe

💻

shivakrishnay

🐛
59 | 60 | 61 | 62 | 63 | 64 | 65 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "sas-folder-tree": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "tsConfig": "tsconfig.app.json", 27 | "aot": true, 28 | "assets": [ 29 | "src/favicon.ico", 30 | "src/assets" 31 | ], 32 | "styles": [ 33 | "node_modules/@clr/icons/clr-icons.min.css", 34 | "node_modules/@clr/ui/clr-ui.min.css", 35 | "src/styles.scss" 36 | ], 37 | "scripts": [ 38 | "node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js", 39 | "node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js", 40 | "node_modules/@clr/icons/clr-icons.min.js" 41 | ], 42 | "es5BrowserSupport": false 43 | }, 44 | "configurations": { 45 | "production": { 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "optimization": true, 53 | "outputHashing": "all", 54 | "sourceMap": false, 55 | "extractCss": true, 56 | "namedChunks": false, 57 | "extractLicenses": true, 58 | "vendorChunk": false, 59 | "buildOptimizer": true, 60 | "budgets": [ 61 | { 62 | "type": "initial", 63 | "maximumWarning": "4mb", 64 | "maximumError": "5mb" 65 | }, 66 | { 67 | "type": "anyComponentStyle", 68 | "maximumWarning": "6kb", 69 | "maximumError": "10kb" 70 | } 71 | ] 72 | } 73 | } 74 | }, 75 | "serve": { 76 | "builder": "@angular-devkit/build-angular:dev-server", 77 | "options": { 78 | "browserTarget": "sas-folder-tree:build" 79 | }, 80 | "configurations": { 81 | "production": { 82 | "browserTarget": "sas-folder-tree:build:production" 83 | } 84 | } 85 | }, 86 | "extract-i18n": { 87 | "builder": "@angular-devkit/build-angular:extract-i18n", 88 | "options": { 89 | "browserTarget": "sas-folder-tree:build" 90 | } 91 | }, 92 | "test": { 93 | "builder": "@angular-devkit/build-angular:karma", 94 | "options": { 95 | "main": "src/test.ts", 96 | "tsConfig": "tsconfig.spec.json", 97 | "karmaConfig": "karma.conf.js", 98 | "assets": [ 99 | "src/favicon.ico", 100 | "src/assets" 101 | ], 102 | "styles": [ 103 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 104 | "src/styles.scss" 105 | ], 106 | "scripts": [] 107 | } 108 | }, 109 | "lint": { 110 | "builder": "@angular-devkit/build-angular:tslint", 111 | "options": { 112 | "tsConfig": [ 113 | "tsconfig.app.json", 114 | "tsconfig.spec.json", 115 | "e2e/tsconfig.json" 116 | ], 117 | "exclude": [ 118 | "**/node_modules/**" 119 | ] 120 | } 121 | }, 122 | "e2e": { 123 | "builder": "@angular-devkit/build-angular:protractor", 124 | "options": { 125 | "protractorConfig": "e2e/protractor.conf.js", 126 | "devServerTarget": "sas-folder-tree:serve" 127 | }, 128 | "configurations": { 129 | "production": { 130 | "devServerTarget": "sas-folder-tree:serve:production" 131 | } 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "defaultProject": "sas-folder-tree" 138 | } -------------------------------------------------------------------------------- /src/app/explorer/explorer.component.scss: -------------------------------------------------------------------------------- 1 | .explorer-root { 2 | display: flex; 3 | height: calc(100vh - 60px); 4 | 5 | hr { 6 | border: 0; 7 | border-bottom: 1px solid #D7D7D7; 8 | } 9 | 10 | .commands { 11 | padding: 10px 40px; 12 | min-width: 350px; 13 | 14 | .actions-wrapper { 15 | padding: 10px 0; 16 | 17 | .current-dir { 18 | display: flex; 19 | align-items: center; 20 | padding: 0 10px; 21 | 22 | clr-icon { 23 | margin-right: 10px; 24 | } 25 | 26 | p { 27 | font-size: 19px; 28 | margin: 0; 29 | } 30 | } 31 | 32 | button { 33 | margin: 0; 34 | } 35 | } 36 | } 37 | 38 | .explorer { 39 | display: flex; 40 | flex-direction: column; 41 | padding: 20px 40px; 42 | background-color: #F8F8F8; 43 | flex: 1; 44 | 45 | .path-wrapper { 46 | display: flex; 47 | align-items: center; 48 | flex-wrap: wrap; 49 | padding-bottom: 10px; 50 | margin-bottom: 20px; 51 | border-bottom: 1px solid #D7D7D7; 52 | 53 | clr-icon { 54 | margin-right: 15px; 55 | } 56 | 57 | .path-point { 58 | padding: 2px 5px; 59 | background-color: #0e364d; 60 | color: #fff; 61 | border-radius: 3px; 62 | position: relative; 63 | cursor: pointer; 64 | // margin-bottom: 10px; 65 | user-select: none; 66 | 67 | &:first-of-type { 68 | margin-right: 10px; 69 | } 70 | 71 | &:not(:first-of-type):not(:last-child) { 72 | margin-right: 20px; 73 | 74 | &::after { 75 | content: ">"; 76 | position: absolute; 77 | color: #00000091; 78 | right: -15px; 79 | } 80 | } 81 | } 82 | } 83 | 84 | .explorer-box { 85 | background-color: #fff; 86 | 87 | .item { 88 | display: flex; 89 | justify-content: space-between; 90 | align-items: center; 91 | padding: 10px 20px; 92 | cursor: pointer; 93 | user-select: none; 94 | 95 | clr-icon { 96 | margin-right: 10px; 97 | } 98 | 99 | &.selected { 100 | background-color: #4883a52b; 101 | 102 | &:hover { 103 | background-color: #4883a52b; 104 | } 105 | } 106 | 107 | &:hover { 108 | background-color: #4883a518; 109 | } 110 | 111 | &:not(:last-child) { 112 | border-bottom: 1px solid #D7D7D7; 113 | } 114 | 115 | clr-dropdown { 116 | position: relative !important; 117 | 118 | clr-dropdown-menu { 119 | transform: none !important; 120 | left: -140px !important; 121 | 122 | .dropdown-item { 123 | &:focus { 124 | outline: none; 125 | } 126 | } 127 | } 128 | 129 | .dropdown-toggle { 130 | &:focus { 131 | outline: none !important; 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | .job-view-row { 139 | display: flex; 140 | align-items: center; 141 | justify-content: space-between; 142 | margin-bottom: 20px; 143 | 144 | p { 145 | margin: 0; 146 | font-size: 23px; 147 | } 148 | 149 | .buttons { 150 | display: flex; 151 | } 152 | } 153 | 154 | .code-wrapper { 155 | flex: 1; 156 | overflow: auto; 157 | border: 1px solid #c3c3c3; 158 | background: white; 159 | // padding: 20px; 160 | 161 | .code-preview { 162 | white-space: pre-wrap; 163 | } 164 | } 165 | 166 | .ace-editor { 167 | flex: 1; 168 | overflow: auto; 169 | } 170 | } 171 | } 172 | 173 | .locked { 174 | pointer-events: none; 175 | opacity: 0.7; 176 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | Homepage 7 | Explorer 8 |
9 |
10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Requests
24 | 25 |
Log out
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/app/explorer/explorer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 36 | 37 |
38 |
39 | 40 |
{{point === '' ? '/' : point}}
41 |
42 | 43 |
44 | 45 | Loading... 46 | 47 |
48 | 49 | 50 |

Folders will appear here.

51 | 52 |
53 |
54 |
55 | 56 | {{item.ITEMNAME}} 57 |
58 | 59 | {{item.ITEMTYPE}} 60 | 61 | 62 | 66 | 67 | 68 | 72 | 76 | 77 | 78 |
79 |
80 |
81 | 82 | 83 |
84 |

{{selectedItem?.ITEMNAME}}

85 | {{selectedItem?.ITEMNAME}} 86 | 87 |
88 | 92 | 93 | 94 | 98 | 102 | 103 |
104 |
105 | 106 |
107 | 108 | 109 | 110 |
111 |
112 |
113 |
114 | 115 | 116 | 117 | 120 | 125 | -------------------------------------------------------------------------------- /src/app/components/requests-modal/requests-modal.component.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 117 | -------------------------------------------------------------------------------- /src/app/explorer/explorer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ClrFocusOnViewInitModule } from '@clr/angular'; 3 | import { SASjsConfig } from '@sasjs/adapter'; 4 | import { SasService } from '../sas.service'; 5 | import { StateService } from '../state.service'; 6 | import cloneDeep from 'lodash-es/cloneDeep' 7 | import { getExecutorPath } from '@sasjs/utils/utils/executor' 8 | 9 | import 'brace'; 10 | import 'brace/mode/markdown'; 11 | import 'brace/theme/monokai'; 12 | import 'brace/ext/searchbox'; 13 | import { ActivatedRoute, Router } from '@angular/router'; 14 | 15 | @Component({ 16 | selector: 'app-explorer', 17 | templateUrl: './explorer.component.html', 18 | styleUrls: ['./explorer.component.scss'] 19 | }) 20 | export class ExplorerComponent implements OnInit { 21 | public itemsTree: DirectoryItem[] = [] 22 | public selectedItem: DirectoryItem | null = null 23 | public selectedJob: string | null = null 24 | public selectedOriginal: string | null = null 25 | 26 | public sasjsConfig: SASjsConfig | null = null 27 | public selectedJobLink: string | null = null 28 | 29 | public treeLoading: boolean = false 30 | public editingJob: boolean = false 31 | public saveLoading: boolean = false 32 | public error: string | null = null 33 | 34 | public urlQuery: { 35 | path: string 36 | jobName: string 37 | } | null = null 38 | 39 | constructor( 40 | private sasService: SasService, 41 | private stateService: StateService, 42 | private route: ActivatedRoute, 43 | private router: Router 44 | ) { 45 | this.route.queryParams.subscribe((params: any) => { 46 | const { path, jobName } = params 47 | 48 | if (path) { 49 | this.urlQuery = { 50 | path: path, 51 | jobName: jobName 52 | } 53 | } 54 | 55 | if (this.itemsTree && this.itemsTree.length > 0) { 56 | this.goToUrlDir() 57 | } 58 | }) 59 | } 60 | 61 | ngOnInit(): void { 62 | this.sasjsConfig = this.sasService.getSasjsConfig() 63 | 64 | this.stateService.startupData.subscribe((rootDirectory: any) => { 65 | this.itemsTree = rootDirectory 66 | 67 | this.goToUrlDir() 68 | }) 69 | } 70 | 71 | goToUrlDir() { 72 | if (this.urlQuery !== null) { 73 | const { path, jobName } = this.urlQuery 74 | 75 | this.openFolder(path, (openSuccess: boolean) => { 76 | let foundJob = this.itemsTree.find((item: DirectoryItem) => item.ITEMNAME === jobName) 77 | 78 | if (foundJob) { 79 | this.selectedItem = foundJob 80 | this.openJob() 81 | } 82 | }) 83 | } 84 | } 85 | 86 | timesClicked: number = 0 87 | selectItem(item: DirectoryItem) { 88 | this.selectedItem = item 89 | 90 | if (this.timesClicked === 1) { 91 | if (this.selectedItem.ITEMTYPE.toLowerCase() === 'folder') this.onOpenFolderClick() 92 | else this.onOpenJobClick() 93 | } 94 | 95 | if (this.timesClicked === 0) { 96 | this.timesClicked++ 97 | 98 | setTimeout(() => { 99 | this.timesClicked = 0 100 | }, 300) 101 | } 102 | } 103 | 104 | jumpToPoint(point: string) { 105 | if (this.editingJob === true) return 106 | 107 | let jumpToPath: string = '/' 108 | 109 | if (point !== '') { 110 | let fullPath = this.getCurrentPath() 111 | let pathUntilClicked = fullPath.slice(0, fullPath.indexOf(point) + 1) 112 | jumpToPath = pathUntilClicked.join('/') 113 | } 114 | 115 | this.selectedJob = null 116 | 117 | this.onOpenFolderClick(jumpToPath) 118 | } 119 | 120 | getCurrentPath(): string[] { 121 | let pathArray: string[] = [''] 122 | 123 | if (!(this.itemsTree && this.itemsTree.length > 0)) return pathArray 124 | 125 | if (this.itemsTree[0].ITEMPATH !== '/') { 126 | pathArray = this.itemsTree[0].ITEMPATH.split('/') 127 | } 128 | 129 | return pathArray 130 | } 131 | 132 | onOpenJobClick() { 133 | if (!this.selectedItem) return 134 | 135 | this.router.navigate( 136 | [], 137 | { 138 | relativeTo: this.route, 139 | queryParams: {jobName: this.selectedItem.ITEMNAME}, 140 | queryParamsHandling: 'merge' 141 | }); 142 | } 143 | 144 | openJob() { 145 | this.treeLoading = true 146 | 147 | if (this.selectedItem === null) { 148 | this.treeLoading = false 149 | return 150 | } 151 | 152 | let folderPath = this.selectedItem.ITEMPATH 153 | let jobName = this.selectedItem.ITEMNAME 154 | 155 | const executorPath = getExecutorPath(this.sasjsConfig?.serverType || '') 156 | 157 | this.selectedJobLink = `${executorPath}/?_PROGRAM=${folderPath}/${jobName}` 158 | 159 | let data = { INDATA: [{ folderpath: folderPath, jobname: jobName}] } 160 | 161 | this.sasService.request('common/getjobcontents', data).then((res: any) => { 162 | this.selectedJob = this.codelinesToString(res.codecontent) 163 | 164 | this.selectedOriginal = cloneDeep(this.selectedJob) 165 | 166 | this.treeLoading = false 167 | }, (err: any) => { 168 | this.treeLoading = false 169 | this.error = err 170 | }) 171 | } 172 | 173 | editJob() { 174 | this.editingJob = true 175 | } 176 | 177 | saveEditJob() { 178 | this.saveLoading = true 179 | 180 | 181 | if (this.selectedItem === null || this.selectedJob === null) { 182 | this.saveLoading = false 183 | return 184 | } 185 | 186 | let folderPath = this.selectedItem.ITEMPATH 187 | let jobName = this.selectedItem.ITEMNAME 188 | 189 | let codeLines = this.selectedJob.split('\n').map((line: string) => { 190 | return {codeline: line} 191 | }) 192 | 193 | let data = { 194 | INDATA: [{ folderpath: folderPath, jobname: jobName}], 195 | SOURCECODE: codeLines 196 | } 197 | 198 | this.sasService.request('edit/postjobcontents', data).then((res: any) => { 199 | if (typeof res.sasjsAbort !== 'undefined') { 200 | this.error = res.sasjsAbort[0].MSG 201 | } else { 202 | this.selectedOriginal = cloneDeep(this.selectedJob) 203 | } 204 | 205 | this.saveLoading = false 206 | }, (err: any) => { 207 | this.saveLoading = false 208 | this.error = err 209 | }) 210 | } 211 | 212 | cancelEditJob() { 213 | this.editingJob = false 214 | this.selectedJob = cloneDeep(this.selectedOriginal) 215 | } 216 | 217 | exitJob() { 218 | if (this.selectedItem === null) return 219 | 220 | this.selectedJobLink = null 221 | 222 | this.onOpenFolderClick(this.selectedItem.ITEMPATH) 223 | } 224 | 225 | goBack() { 226 | if (this.selectedJob !== null) { 227 | this.selectedJob = null 228 | this.exitJob() 229 | 230 | return 231 | } 232 | 233 | if (!this.itemsTree) return 234 | 235 | let path = this.popLastRoutePath(this.itemsTree[0].ITEMPATH) 236 | 237 | this.onOpenFolderClick(path) 238 | } 239 | 240 | onOpenFolderClick(pathOverride?: string) { 241 | let folderPath: string | null = null 242 | 243 | if (this.selectedItem) { 244 | folderPath = this.selectedItem.ITEMPATH === '/' ? 245 | this.selectedItem.ITEMPATH + this.selectedItem.ITEMNAME : 246 | this.selectedItem.ITEMPATH + '/' + this.selectedItem.ITEMNAME 247 | } 248 | 249 | if (pathOverride) folderPath = pathOverride 250 | 251 | if (!folderPath) { 252 | return 253 | } 254 | 255 | this.router.navigate( 256 | [], 257 | { 258 | relativeTo: this.route, 259 | queryParams: {path: folderPath} 260 | }); 261 | } 262 | 263 | openFolder(pathOverride?: string, callback?: any) { 264 | this.treeLoading = true 265 | 266 | let folderPath: string | null = null 267 | 268 | if (this.selectedItem) { 269 | folderPath = this.selectedItem.ITEMPATH === '/' ? 270 | this.selectedItem.ITEMPATH + this.selectedItem.ITEMNAME : 271 | this.selectedItem.ITEMPATH + '/' + this.selectedItem.ITEMNAME 272 | } 273 | 274 | if (pathOverride) folderPath = pathOverride 275 | 276 | if (!folderPath) { 277 | this.treeLoading = false 278 | return 279 | } 280 | 281 | let data = { INDATA: [{folderpath: folderPath}] } 282 | 283 | this.sasService.request('common/getfoldercontents', data).then((res: any) => { 284 | if (res.folders.length > 0) { 285 | this.itemsTree = res.folders 286 | this.selectedItem = null 287 | } else { 288 | this.error = `${folderPath} is empty.` 289 | 290 | let path = this.popLastRoutePath(folderPath || '') 291 | 292 | this.router.navigate( 293 | [], 294 | { 295 | relativeTo: this.route, 296 | queryParams: {path: path} 297 | }); 298 | } 299 | 300 | this.treeLoading = false 301 | 302 | if (callback) callback(true) 303 | }, (err: any) => { 304 | let errString = '' 305 | 306 | if (typeof err === 'object') { 307 | try { 308 | errString = JSON.stringify(err) 309 | } catch(ex) { 310 | errString = 'We are unable to provide you error details' 311 | } 312 | } else { 313 | errString = err 314 | } 315 | 316 | this.treeLoading = false 317 | this.error = errString 318 | 319 | let path = this.popLastRoutePath(folderPath || '') 320 | 321 | this.router.navigate( 322 | [], 323 | { 324 | relativeTo: this.route, 325 | queryParams: {path: path} 326 | }); 327 | 328 | if (callback) callback(false) 329 | }) 330 | } 331 | 332 | codelinesToString(codelines: string[]): string { 333 | return codelines.map((line: any) => line.CODELINE).join('\n') 334 | } 335 | 336 | popLastRoutePath(folderPath: string) { 337 | if (folderPath === '') return '/' 338 | 339 | let path = folderPath 340 | let tempArr = path.split('/') 341 | tempArr.pop() 342 | path = tempArr.join('/') 343 | 344 | if (path === '') path = '/' 345 | 346 | return path 347 | } 348 | } 349 | 350 | export interface DirectoryItem { 351 | ITEMID: string 352 | ITEMNAME: string 353 | ITEMTYPE: string 354 | ITEMPATH: string 355 | } 356 | 357 | // if (this.selectedItem === null) { 358 | // if (pathOverride && jobNameOverride) { 359 | // folderPath = pathOverride 360 | // jobName = jobNameOverride 361 | // } else { 362 | // this.treeLoading = false 363 | // return 364 | // } 365 | // } else { 366 | // folderPath = this.selectedItem.ITEMPATH 367 | // jobName = this.selectedItem.ITEMNAME 368 | // } --------------------------------------------------------------------------------