Merge pull request #2426 from alecmerdler/frontend-refactoring

More Front-end TypeScript Refactoring
This commit is contained in:
Alec Merdler 2017-03-16 14:19:13 -07:00 committed by GitHub
commit cd61482ff1
26 changed files with 1250 additions and 441 deletions

View file

@ -24,11 +24,14 @@ module.exports = function(config) {
'node_modules/raven-js/dist/raven.js',
'node_modules/cal-heatmap/cal-heatmap.js',
// Polyfills
'node_modules/core-js/index.js',
// static/lib resources
'static/lib/**/*.js',
// Application resources
'static/js/**/*.spec.ts*',
// Single entrypoint for all tests
'static/test/test-index.ts',
// Tests utils
'static/test/**/*.js',
@ -37,7 +40,8 @@ module.exports = function(config) {
preprocessors: {
'static/lib/ngReact/react.ngReact.min.js': ['webpack'],
'static/lib/angular-moment.min.js': ['webpack'],
'static/js/**/*.ts*': ['webpack'],
'node_modules/core-js/index.js': ['webpack'],
'static/test/test-index.ts': ['webpack'],
},
webpack: webpackConfig,
webpackMiddleware: {

View file

@ -6,7 +6,7 @@
"scripts": {
"test": "./node_modules/.bin/karma start --single-run --browsers PhantomJS",
"test:node": "JASMINE_CONFIG_PATH=static/test/jasmine.json ./node_modules/.bin/jasmine-ts './static/js/**/*.spec.ts'",
"build": "./node_modules/.bin/webpack --progress -p -v",
"build": "./node_modules/.bin/webpack --progress",
"watch": "./node_modules/.bin/webpack --watch"
},
"repository": {
@ -24,6 +24,7 @@
"bootstrap": "^3.3.2",
"bootstrap-datepicker": "^1.6.4",
"cal-heatmap": "^3.3.10",
"core-js": "^2.4.1",
"d3": "^3.3.3",
"eonasdan-bootstrap-datetimepicker": "^4.17.43",
"jquery": "1.12.4",
@ -37,6 +38,7 @@
"@types/angular": "1.6.2",
"@types/angular-mocks": "^1.5.8",
"@types/angular-route": "^1.3.3",
"@types/angular-sanitize": "^1.3.4",
"@types/es6-shim": "^0.31.32",
"@types/jasmine": "^2.5.41",
"@types/jquery": "^2.0.40",
@ -60,6 +62,7 @@
"source-map-loader": "0.1.5",
"style-loader": "0.13.1",
"ts-loader": "^0.9.5",
"ts-mocks": "^0.2.2",
"typescript": "^2.2.1",
"typings": "1.4.0",
"webpack": "^2.2"

View file

@ -10,10 +10,8 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
restrict: 'C',
scope: {
'repository': '=repository',
'isReady': '=?isReady',
'reset': '=?reset',
'readyForBuild': '&readyForBuild'
},
controller: function($scope, $element, ApiService, DockerfileService, Config) {
@ -39,25 +37,27 @@ angular.module('quay').directive('dockerfileBuildForm', function () {
$scope.state = 'checking';
$scope.selectedFiles = files;
DockerfileService.getDockerfile(files[0], function(df) {
var baseImage = df.getRegistryBaseImage();
if (baseImage) {
checkPrivateImage(baseImage);
} else {
$scope.state = 'ready';
}
DockerfileService.getDockerfile(files[0])
.then(function(dockerfileInfo) {
var baseImage = dockerfileInfo.getRegistryBaseImage();
if (baseImage) {
checkPrivateImage(baseImage);
} else {
$scope.state = 'ready';
}
$scope.$apply(function() {
opt_callback && opt_callback(true, 'Dockerfile found and valid')
});
}, function(msg) {
$scope.state = 'empty';
$scope.privateBaseRepository = null;
$scope.$apply(function() {
opt_callback && opt_callback(true, 'Dockerfile found and valid')
});
})
.catch(function(error) {
$scope.state = 'empty';
$scope.privateBaseRepository = null;
$scope.$apply(function() {
opt_callback && opt_callback(false, msg || 'Could not find valid Dockerfile');
$scope.$apply(function() {
opt_callback && opt_callback(false, error || 'Could not find valid Dockerfile');
});
});
});
};
$scope.handleFilesCleared = function() {

View file

@ -1,4 +1,5 @@
import * as angular from "angular";
import 'core-js';
import { ViewArrayImpl } from "./services/view-array/view-array.impl";
import { NAME_PATTERNS } from "./constants/name-patterns.constant";
import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant";
@ -12,6 +13,10 @@ import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-
import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component';
import { QuayConfig } from './quay-config.module';
import { QuayRun } from './quay-run.module';
import { BuildServiceImpl } from './services/build/build.service.impl';
import { AvatarServiceImpl } from './services/avatar/avatar.service.impl';
import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl';
import { DataFileServiceImpl } from './services/datafile/datafile.service.impl';
/**
@ -33,6 +38,10 @@ import { QuayRun } from './quay-run.module';
],
providers: [
ViewArrayImpl,
BuildServiceImpl,
AvatarServiceImpl,
DockerfileServiceImpl,
DataFileServiceImpl,
],
})
export class quay {
@ -42,6 +51,7 @@ export class quay {
// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2
angular
.module(quay.name)
.factory("fileReaderFactory", () => () => new FileReader())
.constant('NAME_PATTERNS', NAME_PATTERNS)
.constant('INJECTED_CONFIG', INJECTED_CONFIG)
.constant('INJECTED_FEATURES', INJECTED_FEATURES)

View file

@ -1,49 +0,0 @@
/**
* Service which provides helper methods for retrieving the avatars displayed in the app.
*/
angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5',
function(Config, $sanitize, md5) {
var avatarService = {};
var cache = {};
avatarService.getAvatar = function(hash, opt_size, opt_notfound) {
var size = opt_size || 16;
switch (Config['AVATAR_KIND']) {
case 'local':
return '/avatar/' + hash + '?size=' + size;
break;
case 'gravatar':
var notfound = opt_notfound || '404';
return '//www.gravatar.com/avatar/' + hash + '?d=' + notfound + '&size=' + size;
break;
}
};
avatarService.computeHash = function(opt_email, opt_name) {
var email = opt_email || '';
var name = opt_name || '';
var cacheKey = email + ':' + name;
if (!cacheKey) { return '-'; }
if (cache[cacheKey]) {
return cache[cacheKey];
}
var hash = md5.createHash(email.toString().toLowerCase());
switch (Config['AVATAR_KIND']) {
case 'local':
if (name) {
hash = name[0] + hash;
} else if (email) {
hash = email[0] + hash;
}
break;
}
return cache[cacheKey] = hash;
};
return avatarService;
}]);

View file

@ -0,0 +1,97 @@
import { AvatarServiceImpl } from './avatar.service.impl';
describe("AvatarServiceImpl", () => {
var avatarServiceImpl: AvatarServiceImpl;
var configMock: any;
var md5Mock: any;
beforeEach(() => {
configMock = {AVATAR_KIND: 'local'};
md5Mock = jasmine.createSpyObj('md5Mock', ['createHash']);
avatarServiceImpl = new AvatarServiceImpl(configMock, md5Mock);
});
describe("getAvatar", () => {
var hash: string;
beforeEach(() => {
hash = "a1b2c3d4e5f6";
});
it("returns a local avatar URL if given config has avatar kind set to local", () => {
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`/avatar/${hash}?size=16`);
});
it("returns a Gravatar URL if given config has avatar kind set to Gravatar", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=404&size=16`);
});
it("uses 16 as default size query parameter if not provided", () => {
var size: number = 16;
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`/avatar/${hash}?size=${size}`);
});
it("uses 404 as default not found query parameter for Gravatar URL if not provided", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var notFound: string = '404';
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=${notFound}&size=16`);
});
});
describe("computeHash", () => {
var email: string;
var name: string;
var expectedHash: string;
beforeEach(() => {
email = "some_example@gmail.com";
name = "example";
expectedHash = "a1b2c3d4e5f6";
md5Mock.createHash = jasmine.createSpy('createHashSpy').and.returnValue(expectedHash);
});
it("returns hash from cache if it exists", () => {
// Call once to set the cache
avatarServiceImpl.computeHash(email, name);
md5Mock.createHash.calls.reset();
avatarServiceImpl.computeHash(email, name);
expect(md5Mock.createHash).not.toHaveBeenCalled();
});
it("calls MD5 service to create hash using given email if cache is not set", () => {
avatarServiceImpl.computeHash(email, name);
expect(md5Mock.createHash.calls.argsFor(0)[0]).toEqual(email.toString().toLowerCase());
});
it("adds first character of given name to hash if config has avatar kind set to local", () => {
var hash: string = avatarServiceImpl.computeHash(email, name);
expect(hash[0]).toEqual(name[0]);
});
it("adds first character of given email to hash if config has avatar kind set to local and not given name", () => {
var hash: string = avatarServiceImpl.computeHash(email);
expect(hash[0]).toEqual(email[0]);
});
it("adds nothing to hash if config avatar kind is not set to local", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var hash: string = avatarServiceImpl.computeHash(email);
expect(hash).toEqual(expectedHash);
});
});
});

View file

@ -0,0 +1,49 @@
import { AvatarService } from './avatar.service';
import { Injectable } from 'angular-ts-decorators';
@Injectable(AvatarService.name)
export class AvatarServiceImpl implements AvatarService {
private cache: {[cacheKey: string]: string} = {};
constructor(private Config: any, private md5: any) {
}
public getAvatar(hash: string, size: number = 16, notFound: string = '404'): string {
var avatarURL: string;
switch (this.Config['AVATAR_KIND']) {
case 'local':
avatarURL = `/avatar/${hash}?size=${size}`;
break;
case 'gravatar':
avatarURL = `//www.gravatar.com/avatar/${hash}?d=${notFound}&size=${size}`;
break;
}
return avatarURL;
}
public computeHash(email: string = '', name: string = ''): string {
const cacheKey: string = email + ':' + name;
if (this.cache[cacheKey]) {
return this.cache[cacheKey];
}
var hash: string = this.md5.createHash(email.toString().toLowerCase());
switch (this.Config['AVATAR_KIND']) {
case 'local':
if (name) {
hash = name[0] + hash;
} else if (email) {
hash = email[0] + hash;
}
break;
}
return this.cache[cacheKey] = hash;
}
}

View file

@ -0,0 +1,22 @@
/**
* Service which provides helper methods for retrieving the avatars displayed in the app.
*/
export abstract class AvatarService {
/**
* Retrieve URL for avatar image with given hash.
* @param hash Avatar image hash.
* @param size Avatar image size.
* @param notFound URL parameter if avatar image is not found.
* @return avatarURL The URL for the avatar image.
*/
public abstract getAvatar(hash: string, size?: number, notFound?: string): string;
/**
* Compute the avatar image hash.
* @param email Email for avatar user.
* @param name Username for avatar user.
* @return hash The hash for the avatar image.
*/
public abstract computeHash(email?: string, name?: string): string;
}

View file

@ -1,62 +0,0 @@
/**
* Service which provides helper methods for reasoning about builds.
*/
angular.module('quay').factory('BuildService', [function() {
var buildService = {};
buildService.isActive = function(build) {
return build.phase != 'complete' && build.phase != 'error' && build.phase != 'expired' && build.phase != 'cancelled';
};
buildService.getBuildMessage = function(phase) {
switch (phase) {
case 'cannot_load':
return 'Cannot load build status';
case 'starting':
case 'initializing':
return 'Starting Dockerfile build';
case 'waiting':
return 'Waiting for available build worker';
case 'unpacking':
return 'Unpacking build package';
case 'pulling':
return 'Pulling base image';
case 'building':
return 'Building image from Dockerfile';
case 'checking-cache':
return 'Looking up cached images';
case 'priming-cache':
return 'Priming cache for build';
case 'build-scheduled':
return 'Preparing build node';
case 'pushing':
return 'Pushing image built from Dockerfile';
case 'complete':
return 'Dockerfile build completed and pushed';
case 'error':
return 'Dockerfile build failed';
case 'expired':
return 'Build did not complete after 3 attempts. Re-submit this build to try again.';
case 'internalerror':
return 'An internal system error occurred while building; the build will be retried in the next few minutes.';
case 'cancelled':
return 'This build was previously cancelled.';
}
};
return buildService;
}]);

View file

@ -0,0 +1,76 @@
import { BuildServiceImpl } from './build.service.impl';
describe("BuildServiceImpl", () => {
var buildServiceImpl: BuildServiceImpl;
var build: {phase: string};
beforeEach(() => {
buildServiceImpl = new BuildServiceImpl();
build = {phase: ""};
});
describe("isActive", () => {
var phases: string[];
beforeEach(() => {
phases = ['complete', 'error', 'expired', 'cancelled'];
});
it("returns false if given build's phase matches an inactive phase", () => {
phases.forEach((phase: string) => {
build.phase = phase;
expect(buildServiceImpl.isActive(build)).toBe(false);
});
});
it("returns true if given build's phase does not match inactive phases", () => {
build.phase = 'initializing';
expect(buildServiceImpl.isActive(build)).toBe(true);
});
});
describe("getBuildMessage", () => {
var buildMessages: {phase: string, message: string}[];
beforeEach(() => {
buildMessages = [
{phase: 'cannot_load', message: 'Cannot load build status'},
{phase: 'starting', message: 'Starting Dockerfile build'},
{phase: 'initializing', message: 'Starting Dockerfile build'},
{phase: 'waiting', message: 'Waiting for available build worker'},
{phase: 'unpacking', message: 'Unpacking build package'},
{phase: 'pulling', message: 'Pulling base image'},
{phase: 'building', message: 'Building image from Dockerfile'},
{phase: 'checking-cache', message: 'Looking up cached images'},
{phase: 'priming-cache', message: 'Priming cache for build'},
{phase: 'build-scheduled', message: 'Preparing build node'},
{phase: 'pushing', message: 'Pushing image built from Dockerfile'},
{phase: 'complete', message: 'Dockerfile build completed and pushed'},
{phase: 'error', message: 'Dockerfile build failed'},
{phase: 'expired', message: 'Build did not complete after 3 attempts. Re-submit this build to try again.'},
{phase: 'internalerror', message: 'An internal system error occurred while building; the build will be retried in the next few minutes.'},
{phase: 'cancelled', message: 'This build was previously cancelled.'},
];
});
it("returns the correct message for the given phase", () => {
buildMessages.forEach((buildMessage) => {
expect(buildServiceImpl.getBuildMessage(buildMessage.phase)).toEqual(buildMessage.message, buildMessage);
});
});
it("throws an error if given phase is not supported", () => {
var phase: string = "not-a-phase";
try {
buildServiceImpl.getBuildMessage(phase);
fail("Should throw error");
} catch (error) {
expect(error.message).toEqual("Invalid build phase");
}
});
});
});

View file

@ -0,0 +1,84 @@
import { BuildService } from './build.service';
import { Injectable } from 'angular-ts-decorators';
@Injectable(BuildService.name)
export class BuildServiceImpl implements BuildService {
private inactivePhases: string[] = ['complete', 'error', 'expired', 'cancelled'];
public isActive(build: {phase: string}): boolean {
return this.inactivePhases.indexOf(build.phase) == -1;
}
public getBuildMessage(phase: string): string {
var message: string;
switch (phase) {
case 'cannot_load':
message = 'Cannot load build status';
break;
case 'starting':
case 'initializing':
message = 'Starting Dockerfile build';
break;
case 'waiting':
message = 'Waiting for available build worker';
break;
case 'unpacking':
message = 'Unpacking build package';
break;
case 'pulling':
message = 'Pulling base image';
break;
case 'building':
message = 'Building image from Dockerfile';
break;
case 'checking-cache':
message = 'Looking up cached images';
break;
case 'priming-cache':
message = 'Priming cache for build';
break;
case 'build-scheduled':
message = 'Preparing build node';
break;
case 'pushing':
message = 'Pushing image built from Dockerfile';
break;
case 'complete':
message = 'Dockerfile build completed and pushed';
break;
case 'error':
message = 'Dockerfile build failed';
break;
case 'expired':
message = 'Build did not complete after 3 attempts. Re-submit this build to try again.';
break;
case 'internalerror':
message = 'An internal system error occurred while building; the build will be retried in the next few minutes.';
break;
case 'cancelled':
message = 'This build was previously cancelled.';
break;
default:
throw new Error("Invalid build phase");
}
return message;
}
}

View file

@ -0,0 +1,19 @@
/**
* Service which provides helper methods for reasoning about builds.
*/
export abstract class BuildService {
/**
* Determine if the given build is active.
* @param build The build object.
* @return isActive If the given build is active.
*/
public abstract isActive(build: {phase: string}): boolean;
/**
* Generate a message based on a given phase.
* @param phase The phase type.
* @return buildMessage The message associated with the given phase.
*/
public abstract getBuildMessage(phase: string): string;
}

View file

@ -1,176 +0,0 @@
/**
* Service which provides helper methods for downloading a data file from a URL, and extracting
* its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external
* library code in the lib/ directory:
* - jszip.min.js
* - Blob.js
* - zlib.js
*/
angular.module('quay').factory('DataFileService', [function() {
var dataFileService = {};
dataFileService.getName_ = function(filePath) {
var parts = filePath.split('/');
return parts[parts.length - 1];
};
dataFileService.tryAsZip_ = function(buf, success, failure) {
var zip = null;
var zipFiles = null;
try {
var zip = new JSZip(buf);
zipFiles = zip.files;
} catch (e) {
failure();
return;
}
var files = [];
for (var filePath in zipFiles) {
if (zipFiles.hasOwnProperty(filePath)) {
files.push({
'name': dataFileService.getName_(filePath),
'path': filePath,
'canRead': true,
'toBlob': (function(fp) {
return function() {
return new Blob([zip.file(fp).asArrayBuffer()]);
};
}(filePath))
});
}
}
success(files);
};
dataFileService.tryAsTarGz_ = function(buf, success, failure) {
var gunzip = new Zlib.Gunzip(new Uint8Array(buf));
var plain = null;
try {
plain = gunzip.decompress();
} catch (e) {
failure();
return;
}
if (plain.byteLength == 0) {
plain = buf;
}
dataFileService.tryAsTar_(plain, success, failure);
};
dataFileService.tryAsTar_ = function(buf, success, failure) {
var collapsePath = function(originalPath) {
// Tar files can contain entries of the form './', so we need to collapse
// those paths down.
var parts = originalPath.split('/');
for (var i = parts.length - 1; i >= 0; i--) {
var part = parts[i];
if (part == '.') {
parts.splice(i, 1);
}
}
return parts.join('/');
};
var handler = new Untar(new Uint8Array(buf));
handler.process(function(status, read, files, err) {
switch (status) {
case 'error':
failure(err);
break;
case 'done':
var processed = [];
for (var i = 0; i < files.length; ++i) {
var currentFile = files[i];
var path = collapsePath(currentFile.meta.filename);
if (path == '' || path == 'pax_global_header') { continue; }
processed.push({
'name': dataFileService.getName_(path),
'path': path,
'canRead': true,
'toBlob': (function(currentFile) {
return function() {
return new Blob([currentFile.buffer], {type: 'application/octet-binary'});
};
}(currentFile))
});
}
success(processed);
break;
}
});
};
dataFileService.blobToString = function(blob, callback) {
var reader = new FileReader();
reader.onload = function(event){
callback(reader.result);
};
reader.readAsText(blob);
};
dataFileService.arrayToString = function(buf, callback) {
var bb = new Blob([buf], {type: 'application/octet-binary'});
var f = new FileReader();
f.onload = function(e) {
callback(e.target.result);
};
f.onerror = function(e) {
callback(null);
};
f.onabort = function(e) {
callback(null);
};
f.readAsText(bb);
};
dataFileService.readDataArrayAsPossibleArchive = function(buf, success, failure) {
dataFileService.tryAsZip_(buf, success, function() {
dataFileService.tryAsTarGz_(buf, success, function() {
dataFileService.tryAsTar_(buf, success, failure);
});
});
};
dataFileService.downloadDataFileAsArrayBuffer = function($scope, url, progress, error, loaded) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = function(e) {
$scope.$apply(function() {
var percentLoaded;
if (e.lengthComputable) {
progress(e.loaded / e.total);
}
});
};
request.onerror = function() {
$scope.$apply(function() {
error();
});
};
request.onload = function() {
if (this.status == 200) {
$scope.$apply(function() {
var uint8array = new Uint8Array(request.response);
loaded(uint8array);
});
return;
}
};
request.send();
};
return dataFileService;
}]);

View file

@ -0,0 +1,124 @@
import { DataFileServiceImpl } from './datafile.service.impl';
import { Mock } from 'ts-mocks';
import Spy = jasmine.Spy;
describe("DataFileServiceImpl", () => {
var dataFileServiceImpl: DataFileServiceImpl;
var fileReaderMock: Mock<FileReader>;
var fileReader: FileReader;
beforeEach(() => {
fileReaderMock = new Mock<FileReader>();
fileReader = fileReaderMock.Object;
dataFileServiceImpl = new DataFileServiceImpl(() => fileReader);
});
describe("blobToString", () => {
var data: any;
var blob: Blob;
beforeEach(() => {
data = {hello: "world"};
blob = new Blob([JSON.stringify(data)]);
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: data}});
});
});
it("calls file reader to read given blob", (done) => {
dataFileServiceImpl.blobToString(blob, (result) => {
expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob);
done();
});
});
it("calls given callback with null if file reader errors", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onerror(new ErrorEvent("onerror"));
});
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(null);
done();
});
});
it("calls given callback with null if file reader aborts", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onabort(new Event("onabort"));
});
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(null);
done();
});
});
it("calls given callback with result when file reader successfully loads", (done) => {
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(data);
done();
});
});
});
describe("arrayToString", () => {
var blob: Blob;
var data: any;
beforeEach(() => {
data = JSON.stringify({hello: "world"});
blob = new Blob([data], {type: 'application/octet-binary'});
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: data}});
});
});
it("calls file reader to read blob created from given buffer", (done) => {
dataFileServiceImpl.arrayToString(data, (result) => {
expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob);
done();
});
});
it("calls given callback with null if file reader errors", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onerror(new ErrorEvent("onerror"));
});
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(null);
done();
});
});
it("calls given callback with null if file reader aborts", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onabort(new Event("onabort"));
});
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(null);
done();
});
});
it("calls given callback with result when file reader successfully loads", (done) => {
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(data);
done();
});
});
});
describe("readDataArrayAsPossibleArchive", () => {
// TODO
});
describe("downloadDataFileAsArrayBuffer", () => {
// TODO
});
});

View file

@ -0,0 +1,182 @@
import { DataFileService } from './datafile.service';
import { Injectable } from 'angular-ts-decorators';
declare const JSZip: (buf: any) => void;
declare const Zlib: any;
declare const Untar: (uint8Array: Uint8Array) => void;
@Injectable(DataFileService.name)
export class DataFileServiceImpl implements DataFileService {
constructor(private fileReaderFactory: () => FileReader) {
}
public blobToString(blob: Blob, callback: (result: string) => void): void {
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: Event) => callback(event.target['result']);
reader.onerror = (event: Event) => callback(null);
reader.onabort = (event: Event) => callback(null);
reader.readAsText(blob);
}
public arrayToString(buf: any, callback: (result: string) => void): void {
const blob: Blob = new Blob([buf], {type: 'application/octet-binary'});
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: Event) => callback(event.target['result']);
reader.onerror = (event: Event) => callback(null);
reader.onabort = (event: Event) => callback(null);
reader.readAsText(blob);
}
public readDataArrayAsPossibleArchive(buf: any,
success: (result: any) => void,
failure: (error: any) => void): void {
this.tryAsZip(buf, success, () => {
this.tryAsTarGz(buf, success, () => {
this.tryAsTar(buf, success, failure);
});
});
}
public downloadDataFileAsArrayBuffer($scope: ng.IScope,
url: string,
progress: (percent: number) => void,
error: () => void,
loaded: (uint8array: Uint8Array) => void): void {
var request: XMLHttpRequest = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = (e) => {
$scope.$apply(() => {
var percentLoaded;
if (e.lengthComputable) {
progress(e.loaded / e.total);
}
});
};
request.onerror = () => {
$scope.$apply(() => {
error();
});
};
request.onload = function() {
if (request.status == 200) {
$scope.$apply(() => {
var uint8array = new Uint8Array(request.response);
loaded(uint8array);
});
return;
}
};
request.send();
}
private getName(filePath: string): string {
var parts: string[] = filePath.split('/');
return parts[parts.length - 1];
}
private tryAsZip(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var zip = null;
var zipFiles = null;
try {
var zip = new JSZip(buf);
zipFiles = zip.files;
} catch (e) {
failure();
return;
}
var files = [];
for (var filePath in zipFiles) {
if (zipFiles.hasOwnProperty(filePath)) {
files.push({
'name': this.getName(filePath),
'path': filePath,
'canRead': true,
'toBlob': (function(fp) {
return function() {
return new Blob([zip.file(fp).asArrayBuffer()]);
};
}(filePath))
});
}
}
success(files);
}
private tryAsTarGz(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var gunzip = new Zlib.Gunzip(new Uint8Array(buf));
var plain = null;
try {
plain = gunzip.decompress();
} catch (e) {
failure();
return;
}
if (plain.byteLength == 0) {
plain = buf;
}
this.tryAsTar(plain, success, failure);
}
private tryAsTar(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var collapsePath = function(originalPath) {
// Tar files can contain entries of the form './', so we need to collapse
// those paths down.
var parts = originalPath.split('/');
for (var i = parts.length - 1; i >= 0; i--) {
var part = parts[i];
if (part == '.') {
parts.splice(i, 1);
}
}
return parts.join('/');
};
try {
var handler = new Untar(new Uint8Array(buf));
handler.process((status, read, files, err) => {
switch (status) {
case 'error':
failure(err);
break;
case 'done':
var processed = [];
for (var i = 0; i < files.length; ++i) {
var currentFile = files[i];
var path = collapsePath(currentFile.meta.filename);
if (path == '' || path == 'pax_global_header') { continue; }
processed.push({
'name': this.getName(path),
'path': path,
'canRead': true,
'toBlob': (function(currentFile) {
return function() {
return new Blob([currentFile.buffer], {type: 'application/octet-binary'});
};
}(currentFile))
});
}
success(processed);
break;
}
});
} catch (e) {
failure();
}
}
}

View file

@ -0,0 +1,48 @@
/**
* Service which provides helper methods for downloading a data file from a URL, and extracting
* its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external
* library code in the lib/ directory:
* - jszip.min.js
* - Blob.js
* - zlib.js
*/
export abstract class DataFileService {
/**
* Convert a blob to a string.
* @param blob The blob to convert.
* @param callback The success callback given converted blob.
*/
public abstract blobToString(blob: Blob, callback: (result: string) => void): void;
/**
* Convert array to string.
* @param buf The array buffer to convert.
* @param callback The success callback given converted array buffer.
*/
public abstract arrayToString(buf: any, callback: (result: string) => void): void;
/**
* Determine if a given data array is an archive file.
* @param buf The data array to check.
* @param success The success callback if the given array is an archive file, given the file contents.
* @param failure The failure callback if the given array is not an archive file, given the error message.
*/
public abstract readDataArrayAsPossibleArchive(buf: any,
success: (result: any) => void,
failure: (error: any) => void): void;
/**
* Download a file into an array buffer while tracking progress.
* @param $scope An AngularJS $scope instance.
* @param url The URL of the file to be downloaded.
* @param progress The callback for download progress.
* @param error The error callback.
* @param loaded The success callback given the downloaded array buffer.
*/
public abstract downloadDataFileAsArrayBuffer($scope: ng.IScope,
url: string,
progress: (percent: number) => void,
error: () => void,
loaded: (uint8array: Uint8Array) => void): void;
}

View file

@ -1,127 +0,0 @@
/**
* Service which provides helper methods for extracting information out from a Dockerfile
* or an archive containing a Dockerfile.
*/
angular.module('quay').factory('DockerfileService', ['DataFileService', 'Config', function(DataFileService, Config) {
var dockerfileService = {};
function DockerfileInfo(contents) {
this.contents = contents;
}
DockerfileInfo.prototype.getRegistryBaseImage = function() {
var baseImage = this.getBaseImage();
if (!baseImage) {
return null;
}
if (baseImage.indexOf(Config.getDomain() + '/') != 0) {
return null;
}
return baseImage.substring(Config.getDomain().length + 1);
};
DockerfileInfo.prototype.getBaseImage = function() {
var imageAndTag = this.getBaseImageAndTag();
if (!imageAndTag) {
return null;
}
// Note, we have to handle a few different cases here:
// 1) someimage
// 2) someimage:tag
// 3) host:port/someimage
// 4) host:port/someimage:tag
var lastIndex = imageAndTag.lastIndexOf(':');
if (lastIndex < 0) {
return imageAndTag;
}
// Otherwise, check if there is a / in the portion after the split point. If so,
// then the latter is part of the path (and not a tag).
var afterColon = imageAndTag.substring(lastIndex + 1);
if (afterColon.indexOf('/') >= 0) {
return imageAndTag;
}
return imageAndTag.substring(0, lastIndex);
};
DockerfileInfo.prototype.getBaseImageAndTag = function() {
var fromIndex = this.contents.indexOf('FROM ');
if (fromIndex < 0) {
return null;
}
var newline = this.contents.indexOf('\n', fromIndex);
if (newline < 0) {
newline = this.contents.length;
}
return $.trim(this.contents.substring(fromIndex + 'FROM '.length, newline));
};
DockerfileInfo.forData = function(contents) {
if (contents.indexOf('FROM ') < 0) {
return;
}
return new DockerfileInfo(contents);
};
var processFiles = function(files, dataArray, success, failure) {
// The files array will be empty if the submitted file was not an archive. We therefore
// treat it as a single Dockerfile.
if (files.length == 0) {
DataFileService.arrayToString(dataArray, function(c) {
var result = DockerfileInfo.forData(c);
if (!result) {
failure('File chosen is not a valid Dockerfile');
return;
}
success(result);
});
return;
}
var found = false;
files.forEach(function(file) {
if (file['name'] == 'Dockerfile') {
DataFileService.blobToString(file.toBlob(), function(c) {
var result = DockerfileInfo.forData(c);
if (!result) {
failure('Dockerfile inside archive is not a valid Dockerfile');
return;
}
success(result);
});
found = true;
}
});
if (!found) {
failure('No Dockerfile found in root of archive');
}
};
dockerfileService.getDockerfile = function(file, success, failure) {
var reader = new FileReader();
reader.onload = function(e) {
var dataArray = reader.result;
DataFileService.readDataArrayAsPossibleArchive(dataArray, function(files) {
processFiles(files, dataArray, success, failure);
}, function() {
// Not an archive. Read directly as a single file.
processFiles([], dataArray, success, failure);
});
};
reader.onerror = failure;
reader.readAsArrayBuffer(file);
};
return dockerfileService;
}]);

View file

@ -0,0 +1,292 @@
import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl';
import { DataFileService } from '../datafile/datafile.service';
import Spy = jasmine.Spy;
import { Mock } from 'ts-mocks';
describe("DockerfileServiceImpl", () => {
var dockerfileServiceImpl: DockerfileServiceImpl;
var dataFileServiceMock: Mock<DataFileService>;
var dataFileService: DataFileService;
var configMock: any;
var fileReaderMock: Mock<FileReader>;
beforeEach(() => {
dataFileServiceMock = new Mock<DataFileService>();
dataFileService = dataFileServiceMock.Object;
configMock = jasmine.createSpyObj('configMock', ['getDomain']);
fileReaderMock = new Mock<FileReader>();
dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock.Object);
});
describe("getDockerfile", () => {
var file: any;
var invalidArchiveFile: any[];
var validArchiveFile: any[];
var forDataSpy: Spy;
beforeEach(() => {
file = "FROM quay.io/coreos/nginx:latest";
validArchiveFile = [{name: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}];
invalidArchiveFile = [{name: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}];
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
failure([]);
});
dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => callback(""));
dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => callback(blob.toString()));
forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock));
fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: file}});
});
});
it("calls datafile service to read given file as possible archive file", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect((<Spy>fileReaderMock.Object.readAsArrayBuffer).calls.argsFor(0)[0]).toEqual(file);
expect(dataFileService.readDataArrayAsPossibleArchive).toHaveBeenCalled();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("calls datafile service to convert file to string if given file is not an archive", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect((<Spy>dataFileService.arrayToString).calls.argsFor(0)[0]).toEqual(file);
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given non-archive file that is not a valid Dockerfile", (done) => {
forDataSpy.and.returnValue(null);
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail("Promise should be rejected");
done();
})
.catch((error: string) => {
expect(error).toEqual('File chosen is not a valid Dockerfile');
done();
});
});
it("returns resolved promise with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(dockerfile).toBeDefined();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(invalidArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail('Promise should be rejected');
done();
})
.catch((error: string) => {
expect(error).toEqual('No Dockerfile found in root of archive');
done();
});
});
it("calls datafile service to convert blob to string if given file is an archive", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(validArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(validArchiveFile[0].toBlob).toHaveBeenCalled();
expect((<Spy>dataFileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob());
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given archive file with invalid Dockerfile", (done) => {
forDataSpy.and.returnValue(null);
invalidArchiveFile[0].name = 'Dockerfile';
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(invalidArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail('Promise should be rejected');
done();
})
.catch((error: string) => {
expect(error).toEqual('Dockerfile inside archive is not a valid Dockerfile');
done();
});
});
it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(validArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(dockerfile).toBeDefined();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
});
});
describe("DockerfileInfoImpl", () => {
var dockerfileInfoImpl: DockerfileInfoImpl;
var contents: string;
var configMock: any;
beforeEach(() => {
contents = "";
configMock = jasmine.createSpyObj('configMock', ['getDomain']);
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
});
describe("forData", () => {
it("returns null if given contents do not contain a 'FROM' command", () => {
expect(DockerfileInfoImpl.forData(contents, configMock)).toBe(null);
});
it("returns a new DockerfileInfoImpl instance if given contents are valid", () => {
contents = "FROM quay.io/coreos/nginx";
expect(DockerfileInfoImpl.forData(contents, configMock) instanceof DockerfileInfoImpl).toBe(true);
});
});
describe("getRegistryBaseImage", () => {
var domain: string;
var baseImage: string;
beforeEach(() => {
domain = "quay.io";
baseImage = "coreos/nginx";
configMock.getDomain.and.returnValue(domain);
});
it("returns null if instance's contents do not contain a 'FROM' command", () => {
var getBaseImageSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null);
expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null);
expect(getBaseImageSpy).toHaveBeenCalled();
});
it("returns null if the domain of the instance's config does not match that of the base image", () => {
configMock.getDomain.and.returnValue(domain);
spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue('host.com');
expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null);
expect(configMock.getDomain).toHaveBeenCalled();
});
it("returns the registry base image", () => {
spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(`${domain}/${baseImage}`);
expect(dockerfileInfoImpl.getRegistryBaseImage()).toEqual(baseImage);
});
});
describe("getBaseImage", () => {
var host: string;
var port: number;
var tag: string;
var image: string;
beforeEach(() => {
host = 'quay.io';
port = 80;
tag = 'latest';
image = 'coreos/nginx';
});
it("returns null if instance's contents do not contain a 'FROM' command", () => {
var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(null);
expect(dockerfileInfoImpl.getBaseImage()).toBe(null);
expect(getBaseImageAndTagSpy).toHaveBeenCalled();
});
it("returns the image name if in the format 'someimage'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(image);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(image);
});
it("returns the image name if in the format 'someimage:tag'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${image}:${tag}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(image);
});
it("returns the host, port, and image name if in the format 'host:port/someimage'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`);
});
it("returns the host, port, and image name if in the format 'host:port/someimage:tag'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}:${tag}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`);
});
});
describe("getBaseImageAndTag", () => {
it("returns null if instance's contents do not contain a 'FROM' command", () => {
expect(dockerfileInfoImpl.getBaseImageAndTag()).toBe(null);
});
it("returns a string containing the base image and tag from the instance's contents", () => {
contents = "FROM quay.io/coreos/nginx";
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag();
expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.length).trim());
});
it("handles the presence of newlines", () => {
contents = "FROM quay.io/coreos/nginx\nRUN echo $0";
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag();
expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.indexOf('\n')).trim());
});
});
});

View file

@ -0,0 +1,155 @@
import { DockerfileService, DockerfileInfo } from './dockerfile.service';
import { Injectable } from 'angular-ts-decorators';
import { DataFileService } from '../datafile/datafile.service';
@Injectable(DockerfileService.name)
export class DockerfileServiceImpl implements DockerfileService {
constructor(private DataFileService: DataFileService,
private Config: any,
private fileReaderFactory: () => FileReader) {
}
public getDockerfile(file: any): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: any) => {
this.DataFileService.readDataArrayAsPossibleArchive(event.target.result,
(files: any[]) => {
if (files.length > 0) {
this.processFiles(files)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
}
// Not an archive. Read directly as a single file.
else {
this.processFile(event.target.result)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
}
},
() => {
// Not an archive. Read directly as a single file.
this.processFile(event.target.result)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
});
};
reader.onerror = (event: any) => reject(event);
reader.readAsArrayBuffer(file);
});
}
private processFile(dataArray: any): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
this.DataFileService.arrayToString(dataArray, (contents: string) => {
var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config);
if (result == null) {
reject('File chosen is not a valid Dockerfile');
}
else {
resolve(result);
}
});
});
}
private processFiles(files: any[]): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
var found: boolean = false;
files.forEach((file) => {
if (file['name'] == 'Dockerfile') {
this.DataFileService.blobToString(file.toBlob(), (contents: string) => {
var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config);
if (result == null) {
reject('Dockerfile inside archive is not a valid Dockerfile');
}
else {
resolve(result);
}
});
found = true;
}
});
if (!found) {
reject('No Dockerfile found in root of archive');
}
});
}
}
export class DockerfileInfoImpl implements DockerfileInfo {
constructor(private contents: string, private config: any) {
}
public static forData(contents: string, config: any): DockerfileInfoImpl | null {
var dockerfileInfo: DockerfileInfoImpl = null;
if (contents.indexOf('FROM ') != -1) {
dockerfileInfo = new DockerfileInfoImpl(contents, config);
}
return dockerfileInfo;
}
public getRegistryBaseImage(): string | null {
var baseImage = this.getBaseImage();
if (!baseImage) {
return null;
}
if (baseImage.indexOf(this.config.getDomain() + '/') != 0) {
return null;
}
return baseImage.substring(this.config.getDomain().length + 1);
}
public getBaseImage(): string | null {
const imageAndTag = this.getBaseImageAndTag();
if (!imageAndTag) {
return null;
}
// Note, we have to handle a few different cases here:
// 1) someimage
// 2) someimage:tag
// 3) host:port/someimage
// 4) host:port/someimage:tag
const lastIndex: number = imageAndTag.lastIndexOf(':');
if (lastIndex == -1) {
return imageAndTag;
}
// Otherwise, check if there is a / in the portion after the split point. If so,
// then the latter is part of the path (and not a tag).
const afterColon: string = imageAndTag.substring(lastIndex + 1);
if (afterColon.indexOf('/') != -1) {
return imageAndTag;
}
return imageAndTag.substring(0, lastIndex);
}
public getBaseImageAndTag(): string | null {
var baseImageAndTag: string = null;
const fromIndex: number = this.contents.indexOf('FROM ');
if (fromIndex != -1) {
var newlineIndex: number = this.contents.indexOf('\n', fromIndex);
if (newlineIndex == -1) {
newlineIndex = this.contents.length;
}
baseImageAndTag = this.contents.substring(fromIndex + 'FROM '.length, newlineIndex).trim();
}
return baseImageAndTag;
}
}

View file

@ -0,0 +1,38 @@
/**
* Service which provides helper methods for extracting information out from a Dockerfile
* or an archive containing a Dockerfile.
*/
export abstract class DockerfileService {
/**
* Retrieve Dockerfile from given file.
* @param file Dockerfile or archive file containing Dockerfile.
* @return promise Promise which resolves to new DockerfileInfo instance or rejects with error message.
*/
public abstract getDockerfile(file: any): Promise<DockerfileInfo | string>;
}
/**
* Model representing information about a specific Dockerfile.
*/
export abstract class DockerfileInfo {
/**
* Extract the registry base image from the Dockerfile contents.
* @return registryBaseImage The registry base image.
*/
public abstract getRegistryBaseImage(): string | null;
/**
* Extract the base image from the Dockerfile contents.
* @return baseImage The base image.
*/
public abstract getBaseImage(): string | null;
/**
* Extract the base image and tag from the Dockerfile contents.
* @return baseImageAndTag The base image and tag.
*/
public abstract getBaseImageAndTag(): string | null;
}

View file

@ -9,14 +9,14 @@ describe("Service: PageServiceImpl", () => {
});
describe("create", () => {
// TODO
});
describe("get", () => {
// TODO
});
describe("$get", () => {
// TODO
});
});

View file

@ -1,4 +1,6 @@
import { ViewArrayImpl } from "static/js/services/view-array/view-array.impl";
/**
* Specialized wrapper around array which provides a toggle() method for viewing the contents of the
* array in a manner that is asynchronously filled in over a short time period. This prevents long

View file

@ -0,0 +1,6 @@
declare var require: any;
// Require all modules ending in ".spec.ts" from the js directory and all subdirectories
var testsContext = require.context("../js", true, /\.spec\.ts$/);
testsContext.keys().forEach(testsContext);

Binary file not shown.

View file

@ -21,7 +21,9 @@ var config = {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
use: [
"ts-loader",
],
exclude: /node_modules/
},
{

View file

@ -14,6 +14,12 @@
dependencies:
"@types/angular" "*"
"@types/angular-sanitize@^1.3.4":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@types/angular-sanitize/-/angular-sanitize-1.3.4.tgz#aa63bd32a1c3c8846e16d66be891a5bdb3b0fc65"
dependencies:
"@types/angular" "*"
"@types/angular@*", "@types/angular@1.6.2":
version "1.6.2"
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.2.tgz#a5c323ea5d4426ad18984cc8167fa091f7c8201b"
@ -790,7 +796,7 @@ core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
core-js@^2.1.0:
core-js@^2.1.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@ -3874,6 +3880,10 @@ ts-loader@^0.9.5:
object-assign "^4.1.0"
semver "^5.0.1"
ts-mocks@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ts-mocks/-/ts-mocks-0.2.2.tgz#051e5b3a30068f6b9f1b1faa552a6f172793c6d6"
ts-node@^1.2.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-1.7.3.tgz#dee7f8a84751732d3c2e497cac5a02fb117dfee7"