Merge pull request #2426 from alecmerdler/frontend-refactoring
More Front-end TypeScript Refactoring
This commit is contained in:
commit
cd61482ff1
26 changed files with 1250 additions and 441 deletions
|
@ -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: {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}]);
|
97
static/js/services/avatar/avatar.service.impl.spec.ts
Normal file
97
static/js/services/avatar/avatar.service.impl.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
49
static/js/services/avatar/avatar.service.impl.ts
Normal file
49
static/js/services/avatar/avatar.service.impl.ts
Normal 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;
|
||||
}
|
||||
}
|
22
static/js/services/avatar/avatar.service.ts
Normal file
22
static/js/services/avatar/avatar.service.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}]);
|
76
static/js/services/build/build.service.impl.spec.ts
Normal file
76
static/js/services/build/build.service.impl.spec.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
84
static/js/services/build/build.service.impl.ts
Normal file
84
static/js/services/build/build.service.impl.ts
Normal 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;
|
||||
}
|
||||
}
|
19
static/js/services/build/build.service.ts
Normal file
19
static/js/services/build/build.service.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}]);
|
124
static/js/services/datafile/datafile.service.impl.spec.ts
Normal file
124
static/js/services/datafile/datafile.service.impl.spec.ts
Normal 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
|
||||
});
|
||||
});
|
182
static/js/services/datafile/datafile.service.impl.ts
Normal file
182
static/js/services/datafile/datafile.service.impl.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
48
static/js/services/datafile/datafile.service.ts
Normal file
48
static/js/services/datafile/datafile.service.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}]);
|
292
static/js/services/dockerfile/dockerfile.service.impl.spec.ts
Normal file
292
static/js/services/dockerfile/dockerfile.service.impl.spec.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
});
|
155
static/js/services/dockerfile/dockerfile.service.impl.ts
Normal file
155
static/js/services/dockerfile/dockerfile.service.impl.ts
Normal 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;
|
||||
}
|
||||
}
|
38
static/js/services/dockerfile/dockerfile.service.ts
Normal file
38
static/js/services/dockerfile/dockerfile.service.ts
Normal 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;
|
||||
}
|
|
@ -9,14 +9,14 @@ describe("Service: PageServiceImpl", () => {
|
|||
});
|
||||
|
||||
describe("create", () => {
|
||||
|
||||
// TODO
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
|
||||
// TODO
|
||||
});
|
||||
|
||||
describe("$get", () => {
|
||||
|
||||
// TODO
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
6
static/test/test-index.ts
Normal file
6
static/test/test-index.ts
Normal 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.
|
@ -21,7 +21,9 @@ var config = {
|
|||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
use: [
|
||||
"ts-loader",
|
||||
],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||
|
|
Reference in a new issue