├── .dockerignore ├── .gitignore ├── .jshintrc ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── VERSION ├── app ├── .buildignore ├── .htaccess ├── error.html ├── favicon.ico ├── images │ ├── badges_screenshot.png │ ├── cross.svg │ ├── ctl-logo-white.png │ ├── ctl_logo_base.svg │ ├── ctl_logo_base_grey.svg │ ├── example-badge.svg │ ├── favicons │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── favicon-128.png │ │ ├── favicon-16x16.png │ │ ├── favicon-196x196.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ └── mstile-70x70.png │ ├── grid_line.png │ ├── icon_drag.svg │ ├── icon_sprite.svg │ ├── icon_sprite_light_grey.svg │ ├── icon_sprite_red.svg │ ├── icon_sprite_white.svg │ ├── loading.gif │ ├── logo.png │ ├── logo_dray_long.svg │ ├── logo_github_grey.svg │ ├── logo_github_white.svg │ ├── logo_image_layers.svg │ ├── logo_lorry.svg │ ├── logo_twitter_grey.svg │ ├── logo_twitter_white.svg │ ├── microscaling_logo.svg │ ├── panamax_id_02.svg │ ├── plus-white.svg │ └── remove.svg ├── index.html ├── lib │ └── massautocomplete.js ├── robots.txt ├── scripts │ ├── app.js │ ├── controllers │ │ ├── badgedialog.js │ │ ├── dashboard.js │ │ ├── journal.js │ │ └── search.js │ ├── directives │ │ ├── about.js │ │ ├── badge.js │ │ ├── draggable.js │ │ ├── grid.js │ │ ├── imagesearch.js │ │ ├── leaf.js │ │ ├── loadingsrc.js │ │ ├── metrics.js │ │ ├── mobile.js │ │ ├── notification.js │ │ ├── resetfield.js │ │ ├── sticky.js │ │ └── syncScroll.js │ ├── filters │ │ └── size.js │ └── services │ │ ├── commandservice.js │ │ ├── errorinterceptor.js │ │ ├── errorservice.js │ │ ├── gridservice.js │ │ └── registryservice.js ├── styles │ ├── _icons.scss │ ├── _mixins.scss │ ├── _reset.scss │ ├── animations.scss │ ├── application.scss │ ├── dialog.scss │ ├── error.scss │ ├── grid.scss │ ├── header.scss │ ├── main.scss │ └── mobile.scss ├── vendor │ └── ZeroClipboard.swf └── views │ ├── .DS_Store │ ├── about-menu.html │ ├── badge.html │ ├── badgeDialog.html │ ├── dashboard.html │ ├── grid.html │ ├── imageSearch.html │ ├── leaf.html │ ├── metrics.html │ ├── mobile.html │ └── searchDialog.html ├── bin └── deploy_qa.sh ├── bower.json ├── circle.yml ├── deployment ├── README.md ├── badger-deployment.yml ├── badger-svc.yml ├── imagelayers-deployment.yml ├── imagelayers-production-ingress.yml ├── imagelayers-staging-ingress.yml ├── imagelayers-svc.yml ├── imagelayers-web-deployment.yml └── imagelayers-web-svc.yml ├── nginx.conf ├── package.json └── test ├── .DS_Store ├── .jshintrc ├── karma.conf.js └── spec ├── .DS_Store ├── controllers ├── badgedialog.js ├── dashboard.js ├── journal.js └── search.js ├── directives ├── about.js ├── draggable.js ├── grid.js ├── imagesearch.js ├── leaf.js ├── loadingsrc.js ├── metrics.js ├── notification.js ├── resetfield.js ├── sticky.js └── syncScroll.js ├── filters └── size.js └── services ├── commandservice.js ├── errorinterceptor.js ├── errorservice.js ├── gridservice.js └── registryservice.js /.dockerignore: -------------------------------------------------------------------------------- 1 | app/ 2 | bin/ 3 | bower_components/ 4 | node_modules/ 5 | .sass-cache/ 6 | test/ 7 | .tmp/ 8 | bower.json 9 | circle.yml 10 | Gruntfile.js 11 | package.json 12 | README.md 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | config.js 7 | .DS_Store 8 | .bowerrc 9 | .editorconfig 10 | .travis.yml 11 | .yo-rc.json 12 | .gitattributes 13 | .idea/ 14 | .ruby-version 15 | ssl 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.10-alpine 2 | 3 | MAINTAINER Ross Fairbanks 4 | 5 | RUN apk update && \ 6 | apk upgrade && \ 7 | rm -rf /var/cache/apk/* 8 | 9 | EXPOSE 9000 10 | EXPOSE 8080 11 | 12 | # Metadata params 13 | ARG BUILD_DATE 14 | ARG VERSION 15 | ARG VCS_REF 16 | 17 | # Metadata 18 | LABEL org.label-schema.url="https://imagelayers.io" \ 19 | org.label-schema.build-date=$BUILD_DATE \ 20 | org.label-schema.version=$VERSION \ 21 | org.label-schema.vcs-url="https://github.com/microscaling/imagelayers-graph.git" \ 22 | org.label-schema.vcs-ref=$VCS_REF \ 23 | org.label-schema.docker.dockerfile="/Dockerfile" \ 24 | org.label-schema.description="This utility provides a browser-based visualization of user-specified Docker Images and their layers." \ 25 | org.label-schema.schema-version="1.0" 26 | 27 | ADD nginx.conf /etc/nginx/nginx.conf 28 | ADD /dist /data/dist 29 | ADD Dockerfile / 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imagelayers-graph 2 | 3 | [![](https://badge.imagelayers.io/microscaling/imagelayers-web.svg)](https://imagelayers.io/?images=microscaling/imagelayers-web:latest 'Get your own badge on imagelayers.io') [![](https://images.microbadger.com/badges/version/microscaling/imagelayers-web.svg)](http://microbadger.com/images/microscaling/imagelayers-web "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/commit/microscaling/imagelayers-web.svg)](http://microbadger.com/images/microscaling/imagelayers-web "Get your own commit badge on microbadger.com") 4 | 5 | [ImageLayers.io](https://imagelayers.io) is a project maintained by [Microscaling Systems](https://microscaling.com) since September 2016. The project was developed by the team at [CenturyLink Labs](http://www.centurylinklabs.com/). This utility provides a browser-based visualization of user-specified Docker Images and their layers. This visualization provides key information on the composition of a Docker Image and any [commonalities between them](https://imagelayers.io/?images=java:latest,golang:latest,node:latest,python:latest,php:latest,ruby:latest). ImageLayers.io allows Docker users to easily discover best practices for image construction, and aid in determining which images are most appropriate for their specific use cases. Similar to ```docker images --tree```, the ImageLayers project aims to make visualizing your image cache easier, so that you may identify images that take up excessive space and create smarter base images for your Docker projects. 6 | 7 | ## Usage 8 | You can access the hosted version of ImageLayers at [imagelayers.io](http://imagelayers.io). For local development, the imagelayers-graph project requires services provided by the [ImageLayers API](https://github.com/microscaling/imagelayers/). You can inspect images by simply providing a name, with which imagelayers will query and pull from the Docker Hub. The ImageLayers API must be available in order for imagelayers-graph to function. 9 | 10 | ## Building & Development 11 | ImageLayers uses Grunt. To install Grunt, you must first have [npm installed on your machine](https://github.com/npm/npm). Install Grunt with `npm install -g grunt-cli`. Next, install dependencies using [Bower](http://bower.io/#install-bower) with `bower install`. 12 | 13 | The last step is to install Compass. ImageLayers recommends using the latest version of Ruby. 14 | `gem install compass` 15 | 16 | Next, make sure the [imagelayers API](https://github.com/microscaling/imagelayers/) is running. 17 | Run `grunt` for building the UI and `grunt serve` for preview. The ImageLayers UI will automatically open in a browser window. 18 | 19 | ### Building the Docker image 20 | 21 | Use grunt to build the Angular app into the dist directory which is added to the Docker image. The build args are used to label the image. 22 | 23 | ``` 24 | $ grunt 25 | 26 | $ docker build --tag microscaling/imagelayers-web:$(cat VERSION) \ 27 | --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \ 28 | --build-arg VCS_REF=`git rev-parse --short HEAD` \ 29 | --build-arg VERSION=`cat VERSION` . 30 | ``` 31 | 32 | ### Deploying in Kubernetes 33 | 34 | Please see deployment/README.md 35 | 36 | ## Testing 37 | Running `grunt test` will run the unit tests with karma. 38 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.2 2 | -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ImageLayers | Error 8 | 9 | 10 |
11 |

ImageLayers

12 |

Redirecting to ImageLayers. If you are not redirected, please click here.

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/favicon.ico -------------------------------------------------------------------------------- /app/images/badges_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/badges_screenshot.png -------------------------------------------------------------------------------- /app/images/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/images/ctl-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/ctl-logo-white.png -------------------------------------------------------------------------------- /app/images/ctl_logo_base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 25 | 28 | 30 | 32 | 34 | 36 | 37 | 38 | 39 | 41 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/images/ctl_logo_base_grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 24 | 27 | 29 | 31 | 33 | 35 | 36 | 37 | 38 | 40 | 42 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/images/example-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ImageLayers.io 8 | 95 MB / 21 Layers 9 | 10 | -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /app/images/favicons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /app/images/favicons/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/favicon-128.png -------------------------------------------------------------------------------- /app/images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /app/images/favicons/favicon-196x196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/favicon-196x196.png -------------------------------------------------------------------------------- /app/images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /app/images/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /app/images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /app/images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /app/images/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /app/images/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /app/images/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /app/images/grid_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/grid_line.png -------------------------------------------------------------------------------- /app/images/icon_drag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/loading.gif -------------------------------------------------------------------------------- /app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/images/logo.png -------------------------------------------------------------------------------- /app/images/logo_dray_long.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 13 | 19 | 22 | 26 | 28 | 36 | 41 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/images/logo_github_grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/images/logo_github_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /app/images/logo_image_layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 20 | 21 | 22 | 26 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/images/logo_lorry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/images/logo_twitter_grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/images/logo_twitter_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /app/images/panamax_id_02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /app/images/plus-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/images/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ImageLayers | A Docker Image Visualizer 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 | Learn more about ImageLayers 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular 5 | .module('iLayers', [ 6 | 'ngRoute', 7 | 'ngAnimate', 8 | 'ngDialog', 9 | 'MassAutoComplete', 10 | 'luegg.directives', 11 | 'config', 12 | 'zeroclipboard' 13 | ]) 14 | .config(configure); 15 | 16 | configure.$inject = ['$httpProvider', '$locationProvider', '$routeProvider', 'uiZeroclipConfigProvider']; 17 | 18 | function configure($httpProvider, $locationProvider, $routeProvider, uiZeroclipConfigProvider) { 19 | $httpProvider.interceptors.push('errorInterceptor'); 20 | $httpProvider.defaults.withCredentials = false; 21 | 22 | $locationProvider.html5Mode(true); 23 | 24 | $routeProvider 25 | .when ('/', { 26 | templateUrl: 'views/dashboard.html', 27 | controller: 'DashboardCtrl' 28 | 29 | }) 30 | .otherwise ({ 31 | redirectTo: '/' 32 | }); 33 | 34 | uiZeroclipConfigProvider.setZcConf({ 35 | swfPath: 'vendor/ZeroClipboard.swf' 36 | }); 37 | } 38 | })(); -------------------------------------------------------------------------------- /app/scripts/controllers/badgedialog.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc function 6 | * @name iLayers.controller:BadgedialogCtrl 7 | * @description 8 | * # BadgedialogCtrl 9 | * Controller of the iLayers 10 | */ 11 | angular.module('iLayers') 12 | .controller('BadgeDialogCtrl', BadgeDialogCtrl); 13 | 14 | BadgeDialogCtrl.$inject = ['$scope', '$sce']; 15 | 16 | function BadgeDialogCtrl($scope, $sce) { 17 | var nameChanged = function(current, previous) { 18 | var newName = current.name, 19 | oldName = previous.name; 20 | 21 | return (newName !== undefined && 22 | newName !== oldName); 23 | }; 24 | 25 | var newImage = function() { 26 | return { 27 | name: '', 28 | tag: 'latest' 29 | }; 30 | }; 31 | 32 | $scope.selectedImage = newImage(); 33 | 34 | $scope.imageList = function() { 35 | var data = $scope.graph, 36 | list = []; 37 | 38 | angular.forEach(data, function(image) { 39 | image.repo.label = image.repo.name + ':' + image.repo.tag; 40 | list.push(image.repo); 41 | }); 42 | 43 | if (list.length < 1) { 44 | $scope.selectedWorkflow = 'hub'; 45 | } 46 | 47 | return list; 48 | }; 49 | 50 | $scope.$watch('selectedWorkflow', function() { 51 | $scope.selectedImage = newImage(); 52 | }); 53 | 54 | $scope.$watch('selectedImage', function(newValue, oldValue) { 55 | var image = $scope.selectedImage; 56 | 57 | if ($scope.selectedWorkflow === 'imagelayers' && image.name.length > 0) { 58 | $scope.selectedImage.selected = true; 59 | } 60 | 61 | if ($scope.selectedWorkflow === 'hub') { 62 | if (image.missing || nameChanged(newValue, oldValue)) { 63 | $scope.selectedImage.selected = false; 64 | } 65 | } 66 | 67 | $scope.htmlCopied = false; 68 | $scope.markdownCopied = false; 69 | $scope.asciiDocCopied = false; 70 | }, true); 71 | 72 | $scope.badgeAsHtml = function() { 73 | if ($scope.selectedImage.selected !== true) { 74 | return ""; 75 | } 76 | 77 | return $sce.trustAsHtml("" + 78 | ""); 79 | }; 80 | 81 | $scope.badgeAsMarkdown = function() { 82 | if ($scope.selectedImage.selected !== true) { 83 | return ""; 84 | } 85 | 86 | return "[![](https://badge.imagelayers.io/" + $scope.selectedImage.name + ":" + $scope.selectedImage.tag + ".svg)]" + 87 | "(https://imagelayers.io/?images=" + $scope.selectedImage.name + ":" + $scope.selectedImage.tag + " 'Get your own badge on imagelayers.io')"; }; 88 | 89 | $scope.badgeAsAsciiDoc = function() { 90 | if ($scope.selectedImage.selected !== true) { 91 | return ""; 92 | } 93 | 94 | return 'image:https://badge.imagelayers.io/' + $scope.selectedImage.name + ':' + $scope.selectedImage.tag + '.svg' + 95 | '[title="Get your own badge on imagelayers.io", alt="Get your own badge on imagelayers.io", link="https://imagelayers.io/?images=' + $scope.selectedImage.name + ':' + $scope.selectedImage.tag + '"]'; 96 | } 97 | } 98 | })(); 99 | -------------------------------------------------------------------------------- /app/scripts/controllers/dashboard.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .controller('DashboardCtrl', DashboardCtrl); 6 | 7 | DashboardCtrl.$inject = ['$scope', '$routeParams', '$window','registryService', 'commandService']; 8 | 9 | function DashboardCtrl($scope, $routeParams, $window, registryService, commandService) { 10 | var self = this; 11 | $scope.loading = false; 12 | $scope.empty = true; 13 | 14 | //private 15 | self.buildTerms = function(data) { 16 | var terms = data.split(','), 17 | searchTerms = []; 18 | 19 | for (var i=0; i < terms.length; i++) { 20 | var name = terms[i].split(':')[0], 21 | tag = 'latest'; 22 | if (terms[i].lastIndexOf(':') !== -1) { 23 | tag = terms[i].split(':')[1]; 24 | } 25 | searchTerms.push({ 26 | 'name': name.trim(), 27 | 'tag': tag 28 | }); 29 | } 30 | 31 | return searchTerms; 32 | }; 33 | 34 | self.searchImages = function(route) { 35 | var searchTerms; 36 | 37 | if (route.images !== undefined) { 38 | $scope.loading = true; 39 | $scope.empty = false; 40 | searchTerms = self.buildTerms(route.images); 41 | 42 | // Load Data 43 | registryService.inspect(searchTerms).then(function(response) { 44 | $scope.graph = response.data; 45 | $scope.loading = false; 46 | }); 47 | } 48 | }; 49 | 50 | // Load data from RouteParams 51 | self.searchImages($routeParams); 52 | 53 | //mobile device check 54 | self.detectMobile = function(){ 55 | var userAgent = $window.navigator.userAgent; 56 | var mobileRegex = new RegExp('webOS|ip(hone|od|ad)|android|iemobile', 'i'); 57 | $scope.mobile = mobileRegex.test(userAgent); 58 | }; 59 | self.detectMobile(); 60 | 61 | // public 62 | $scope.graph = []; 63 | 64 | $scope.filters = { 65 | 'image': '' 66 | }; 67 | 68 | $scope.applyFilters = function(graphData, filter) { 69 | var filteredData = [], 70 | element = {}, 71 | key = '', 72 | locked = commandService.locked(); 73 | 74 | for (var i=0; i < graphData.length; i ++) { 75 | element = graphData[i].repo; 76 | key = element.name + ':' + element.tag; 77 | if (key.lastIndexOf(filter) !== -1) { 78 | filteredData.push(graphData[i]); 79 | } 80 | } 81 | 82 | $scope.$evalAsync(function() { 83 | commandService.release(); 84 | commandService.lock(locked); 85 | }); 86 | 87 | return filteredData; 88 | }; 89 | 90 | $scope.showCommands = function(repo) { 91 | var data = $scope.graph; 92 | 93 | for (var i=0; i < data.length; i++) { 94 | if (data[i].repo.name === repo.name && data[i].repo.tag === repo.tag) { 95 | commandService.highlight(data[i].layers); 96 | break; 97 | } 98 | } 99 | }; 100 | } 101 | })(); 102 | 103 | -------------------------------------------------------------------------------- /app/scripts/controllers/journal.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc function 6 | * @name iLayers.controller:JournalCtrl 7 | * @description 8 | * # JournalCtrl 9 | * Controller of the iLayers 10 | */ 11 | angular.module('iLayers') 12 | .controller('JournalCtrl', JournalCtrl); 13 | 14 | JournalCtrl.$inject = ['$scope']; 15 | 16 | function JournalCtrl($scope) { 17 | $scope.commands = []; 18 | 19 | $scope.$on('command-change', function(event, data) { 20 | if ($scope.lockedImage === undefined) { 21 | $scope.commands = data.commands; 22 | } 23 | }); 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /app/scripts/controllers/search.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .controller('SearchCtrl', SearchCtrl); 6 | 7 | SearchCtrl.$inject = ['$window', '$scope', '$location', 'ngDialog']; 8 | 9 | function SearchCtrl($window, $scope, $location, ngDialog) { 10 | var self = this; 11 | 12 | $scope.URL = $location.absUrl(); 13 | 14 | self.buildQueryParams = function(list) { 15 | var params = []; 16 | for (var i=0; i < list.length; i++) { 17 | if (list[i].tag === '') { 18 | params.push(list[i].name); 19 | } else { 20 | params.push(list[i].name + ':' + list[i].tag); 21 | } 22 | } 23 | 24 | return params.join(','); 25 | }; 26 | 27 | self.populateSearch = function() { 28 | var terms = $location.search(), 29 | searchList = []; 30 | if (terms.images) { 31 | var images = terms.images.split(','); 32 | angular.forEach(images, function(value) { 33 | var parts = value.split(':'), 34 | tag = 'latest'; 35 | if (parts.length === 2) { 36 | tag = parts[1]; 37 | } 38 | searchList.push(self.makeImage(parts[0], tag)); 39 | }); 40 | } else { 41 | searchList.push(self.makeImage('', 'latest')); 42 | } 43 | 44 | return searchList; 45 | }; 46 | 47 | self.makeImage = function(name, tag) { 48 | return { 49 | 'name': name, 50 | 'tag': tag 51 | }; 52 | }; 53 | 54 | $scope.searchList = self.populateSearch(); 55 | 56 | $scope.removeAll = function() { 57 | $scope.searchList = [{ name: '', tag: 'latest' }]; 58 | }; 59 | 60 | $scope.showSearch = function() { 61 | ngDialog.open({ 62 | closeByDocument: false, 63 | template: 'views/searchDialog.html', 64 | className: 'ngdialog-theme-layers', 65 | controller: 'SearchCtrl' }); 66 | }; 67 | 68 | $scope.addRow = function() { 69 | $scope.searchList.push(self.makeImage('', 'latest')); 70 | }; 71 | 72 | $scope.closeDialog = function() { 73 | ngDialog.closeAll(); 74 | }; 75 | 76 | $scope.showExampleSearch = function () { 77 | $scope.searchList = [ 78 | {name: 'microscaling/imagelayers-api', tag:'latest', found: true}, 79 | {name: 'microscaling/imagelayers-web', tag:'latest', found: true} 80 | ]; 81 | $scope.addImages(); 82 | }; 83 | 84 | $scope.addImages = function() { 85 | var sanitizedList = []; 86 | 87 | $location.search('lock', null); 88 | 89 | angular.forEach($scope.searchList, function(value) { 90 | if (value.name !== '' && value.missing !== true) { 91 | this.push(value); 92 | // GA Event 93 | if (undefined !== $window.ga) { 94 | $window.ga('send', 'event', 'image', 'analyze', value.name, 1); 95 | } 96 | } 97 | }, sanitizedList); 98 | 99 | if (sanitizedList.length > 0) { 100 | $location.search('images', self.buildQueryParams(sanitizedList)); 101 | } else { 102 | $location.url($location.path()); 103 | } 104 | $scope.closeDialog(); 105 | }; 106 | 107 | $scope.removeImage = function(index) { 108 | $scope.searchList.splice(index,1); 109 | }; 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /app/scripts/directives/about.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:about 7 | * @description 8 | * # about 9 | */ 10 | angular.module('iLayers') 11 | .directive('about', About); 12 | 13 | About.$inject = ['$timeout', '$templateRequest']; 14 | 15 | function About($timeout, $templateRequest) { 16 | return { 17 | restrict: 'A', 18 | scope: {}, 19 | controller: function() { 20 | $templateRequest('views/about-menu.html'); 21 | }, 22 | link: function postLink(scope, element) { 23 | var main = element.closest('main'), 24 | headerHeight = $('header').height(), 25 | html = $('html'); 26 | 27 | scope.menuVisible = false; 28 | 29 | scope.toggleMenu = function(aboutMenu) { 30 | aboutMenu.slideToggle(); 31 | scope.menuVisible = !scope.menuVisible; 32 | }; 33 | 34 | element.bind('click', function(e) { 35 | e.stopPropagation(); 36 | var aboutMenu = $('div.about'); 37 | scope.toggleMenu(aboutMenu); 38 | }); 39 | 40 | html.bind('click', function(e) { 41 | var aboutMenu = $('div.about'); 42 | 43 | if (scope.menuVisible && !aboutMenu.is(e.target) && aboutMenu.has(e.target).length === 0) { 44 | aboutMenu.slideUp(); 45 | scope.menuVisible = !scope.menuVisible; 46 | } 47 | }); 48 | 49 | main.bind('scroll', function () { 50 | if (scope.menuVisible) { 51 | if (headerHeight*3 < $('main').scrollTop()) { 52 | var aboutMenu = $('div.about'); 53 | scope.toggleMenu(aboutMenu); 54 | } 55 | } 56 | }) 57 | } 58 | }; 59 | } 60 | })(); 61 | -------------------------------------------------------------------------------- /app/scripts/directives/badge.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:badge 7 | * @description 8 | * # badge 9 | */ 10 | angular.module('iLayers').directive('badge', Badge); 11 | 12 | Badge.$inject = ['ngDialog']; 13 | 14 | function Badge(ngDialog) { 15 | return { 16 | templateUrl: 'views/badge.html', 17 | restrict: 'E', 18 | replace: 'true', 19 | controller: function($scope) { 20 | var dialogID; 21 | 22 | $scope.open = function() { 23 | dialogID = ngDialog.open({ 24 | template: 'views/badgeDialog.html', 25 | className: 'ngdialog-theme-layers badge-dialog', 26 | controller: 'BadgeDialogCtrl', 27 | scope: $scope 28 | }); 29 | }; 30 | } 31 | }; 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /app/scripts/directives/draggable.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:draggable 7 | * @description 8 | * # draggable 9 | */ 10 | angular.module('iLayers') 11 | .directive('draggable', Draggable); 12 | 13 | Draggable.$inject = ['$window']; 14 | 15 | function Draggable($window) { 16 | return { 17 | template: '
', 18 | restrict: 'E', 19 | scope: {}, 20 | replace: true, 21 | link: function postLink(scope, element) { 22 | var handle = element.find('.drag-handle'), 23 | offset = 0; 24 | 25 | scope.updatePosition = function(pos) { 26 | $('main').css('bottom', pos + 'px'); 27 | $('footer#journal-wrapper').css('height', pos + 'px'); 28 | }; 29 | 30 | handle.bind('mousedown', function(e) { 31 | var $elem = $(e.currentTarget); 32 | 33 | element.addClass('dragging'); 34 | offset = e.pageY - $elem.offset().top; 35 | }); 36 | 37 | $('body').bind('mouseup', function() { 38 | element.removeClass('dragging'); 39 | }); 40 | 41 | $('body').bind('mousemove', function(e) { 42 | var height = $window.innerHeight, 43 | bottom = height - (e.pageY - offset); 44 | 45 | if (element.hasClass('dragging')) { 46 | scope.updatePosition(bottom); 47 | } 48 | }); 49 | } 50 | }; 51 | } 52 | })(); 53 | -------------------------------------------------------------------------------- /app/scripts/directives/grid.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .directive('grid', Grid); 6 | 7 | Grid.$inject = ['$timeout', '$sce', '$routeParams', 'commandService', 'gridService']; 8 | 9 | function Grid($timeout, $sce, $routeParams, commandService, gridService) { 10 | /*jshint camelcase: false */ 11 | var constants = { 12 | colWidth: 205, 13 | boxWidth: 160 14 | }; 15 | 16 | var startsWith = function(str, text) { 17 | var list = text.split(' '), 18 | retval = false; 19 | 20 | angular.forEach(list, function(item) { 21 | retval = retval || (str.indexOf(item) === 0); 22 | }); 23 | 24 | return retval; 25 | }; 26 | 27 | return { 28 | templateUrl: 'views/grid.html', 29 | restrict: 'A', 30 | replace: false, 31 | controller: function($scope) { 32 | var self = this; 33 | 34 | self.classifyLayer = function(layer, count) { 35 | var classes = ['box'], 36 | cmd = self.getCommand(layer); 37 | 38 | if (count === 0) { 39 | return 'noop'; 40 | } 41 | 42 | if (startsWith(cmd, 'RUN')) { 43 | classes.push('cat1'); 44 | } 45 | 46 | if (startsWith(cmd, 'ADD COPY')) { 47 | classes.push('cat2'); 48 | } 49 | 50 | if (startsWith(cmd, 'VOLUME ENV USER WORKDIR')) { 51 | classes.push('cat3'); 52 | } 53 | 54 | if (startsWith(cmd, 'ENTRYPOINT CMD')) { 55 | classes.push('cat4'); 56 | } 57 | 58 | if (startsWith(cmd, 'FROM MAINTAINER EXPOSE ONBUILD')) { 59 | classes.push('cat5'); 60 | } 61 | 62 | return classes.join(' '); 63 | }; 64 | 65 | self.getCommand = function(layer) { 66 | var command = (layer.container_config === undefined) ? [] : (layer.container_config.Cmd !== null) ? layer.container_config.Cmd.join(' ') : ''; 67 | return commandService.constructCommand(command, layer.Size); 68 | }; 69 | 70 | self.findWidth = function(count) { 71 | if (count === 0) { 72 | return 0; 73 | } 74 | return count * constants.boxWidth + (count-1)*40; 75 | }; 76 | 77 | self.addDisplayProperties = function(layer) { 78 | layer.cmd = $sce.trustAsHtml(self.getCommand(layer)); 79 | return layer; 80 | }; 81 | 82 | $scope.checkLockParam =function() { 83 | if ($routeParams.lock) { 84 | var lock = $routeParams.lock.split(':'), 85 | tag = (lock.length > 1) ? lock[1] : 'latest'; 86 | commandService.lock({ name: lock[0], tag: tag }); 87 | } 88 | }; 89 | 90 | $scope.unwrapGrid = function(grid) { 91 | var data = [], 92 | map = []; 93 | 94 | if (grid.matrix) { 95 | 96 | map = grid.matrix.map; 97 | 98 | for (var row=0; row < grid.rows; row++) { 99 | for (var col=0; col < grid.cols; col++) { 100 | var layer = map[col][row].layer, 101 | count =0; 102 | 103 | if (grid.matrix.inventory[layer.id] !== undefined) { 104 | count = grid.matrix.inventory[layer.id].count; 105 | grid.matrix.inventory[layer.id].count = 0; 106 | } 107 | 108 | data.push({ 'type': self.classifyLayer(layer, count), 109 | 'width': self.findWidth(count), 110 | 'layer': self.addDisplayProperties(layer) }); 111 | } 112 | } 113 | } 114 | 115 | return data; 116 | }; 117 | }, 118 | link: function(scope, element) { 119 | scope.highlightCommand = function(layerId) { 120 | var item = gridService.inventory(layerId), 121 | start = 0, 122 | layers = []; 123 | 124 | if (item !== undefined) { 125 | layers = item.image.layers; 126 | // find start in layers // 127 | for (var l=0; l < layers.length; l++) { 128 | if (layerId === layers[l].id) { 129 | start = l; 130 | } 131 | } 132 | 133 | commandService.highlight(item.image.layers.slice(0,start+1)); 134 | } 135 | }; 136 | 137 | scope.clearCommands = function() { 138 | commandService.clear(); 139 | }; 140 | 141 | scope.buildGrid = function(graph) { 142 | var gridData = gridService.buildGrid(graph); 143 | 144 | element.find('.matrix').css('min-width', (gridData.cols * constants.colWidth) + 'px'); 145 | 146 | scope.leaves = gridService.findLeaves(gridData); 147 | return scope.unwrapGrid(gridData); 148 | }; 149 | 150 | scope.$watch('graph', function(graph, oldGraph) { 151 | scope.grid = scope.buildGrid(graph); 152 | if (oldGraph.length === 0) { 153 | scope.$evalAsync(scope.checkLockParam); 154 | } 155 | }); 156 | 157 | scope.$watch('filters.image', function(filter) { 158 | if (scope.graph !== undefined) { 159 | var graphData = scope.applyFilters(scope.graph, filter); 160 | scope.grid = scope.buildGrid(graphData); 161 | } 162 | }); 163 | 164 | scope.$on('lock-image', function(evt, data) { 165 | var graph = scope.graph, 166 | lockedLayers = {}; 167 | if (data.image) { 168 | for (var i=0; i < graph.length; i++) { 169 | if (data.image.name === graph[i].repo.name && data.image.tag === graph[i].repo.tag) { 170 | var layers = graph[i].layers; 171 | for (var l=0; l < layers.length; l++) { 172 | lockedLayers[layers[l].id] = layers[l].id; 173 | } 174 | break; 175 | } 176 | } 177 | } 178 | 179 | angular.forEach(scope.grid, function(panel) { 180 | var layer = panel.layer; 181 | 182 | if (lockedLayers[layer.id] !== undefined) { 183 | panel.type = panel.type + ' locked'; 184 | } else { 185 | panel.type = panel.type.replace('locked', ''); 186 | } 187 | }); 188 | }); 189 | } 190 | }; 191 | } 192 | })(); 193 | -------------------------------------------------------------------------------- /app/scripts/directives/imagesearch.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module ('iLayers') 5 | .directive('imageSearch', ImageSearch); 6 | 7 | ImageSearch.$inject = ['$sce', 'registryService']; 8 | 9 | function ImageSearch($sce, registryService) { 10 | return { 11 | restrict: 'A', 12 | scope: { 13 | model: '=', 14 | }, 15 | templateUrl: 'views/imageSearch.html', 16 | link: function(scope, element, attrs) { 17 | var constants = { 18 | offset: 41, 19 | termLimit: 2 20 | }; 21 | 22 | var attached = function(element) { 23 | $('.ac-container').css('top', (element[0].offsetTop + constants.offset) + 'px'); 24 | }; 25 | 26 | var clearError = function() { 27 | delete scope.model.missing; 28 | }; 29 | 30 | var selected = function(item) { 31 | if (scope.withTags) { 32 | loadTags(); 33 | } else { 34 | clearError(); 35 | scope.model.selected = true; 36 | scope.model.name = item.value; 37 | } 38 | }; 39 | 40 | var loadTags = function() { 41 | clearError(); 42 | element.find('.styled-select').addClass('searching'); 43 | registryService.fetchTags(scope.model.name).then(function(response) { 44 | scope.tagList = []; 45 | 46 | if (response.data) { 47 | var data = Object.keys(response.data); 48 | for (var i=0; i < data.length; i++) { 49 | scope.tagList.push(data[i]); 50 | clearError(); 51 | element.find('.styled-select').removeClass('searching'); 52 | scope.model.selected = true; 53 | } 54 | } else { 55 | scope.model.missing = true; 56 | } 57 | }); 58 | }; 59 | 60 | scope.withTags = (attrs.withTags === undefined) ? false : true; 61 | 62 | scope.suggestImages = function(term) { 63 | 64 | if (term.length > constants.termLimit) { 65 | element.find('.image-name').addClass('searching'); 66 | 67 | return registryService.search(term).then(function(response) { 68 | var data = response.data.results, 69 | list = [], 70 | found = false; 71 | 72 | for (var i=0; i < data.length; i++) { 73 | list.push({ 'label': $sce.trustAsHtml(data[i].name), 'value': data[i].name}); 74 | if (term === data[i].name) { 75 | found = true; 76 | } 77 | } 78 | 79 | scope.model.missing = !found; 80 | 81 | element.find('.image-name').removeClass('searching'); 82 | return list; 83 | }); 84 | } else { 85 | return []; 86 | } 87 | }; 88 | 89 | scope.tagList = []; 90 | 91 | scope.autocomplete_options = { // jshint ignore:line 92 | 'suggest': scope.suggestImages, 93 | 'on_error': console.log, 94 | 'on_attach': attached, 95 | 'on_select': selected 96 | }; 97 | 98 | scope.initialValue = function(newValue, oldValue) { 99 | return newValue !== undefined && newValue === oldValue && newValue.length > constants.termLimit; 100 | }; 101 | 102 | scope.$watch('model.name', function(newValue, oldValue) { 103 | if (scope.withTags && scope.initialValue(newValue, oldValue)) { 104 | loadTags(); 105 | } else { 106 | if (scope.model) { 107 | scope.model.tag = 'latest'; 108 | } 109 | } 110 | }); 111 | 112 | if (scope.model && scope.model.name.length === 0) { 113 | element.find('.image-name')[0].focus(); 114 | } 115 | } 116 | }; 117 | } 118 | })(); -------------------------------------------------------------------------------- /app/scripts/directives/leaf.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:leaf 7 | * @description 8 | * # leaf 9 | */ 10 | angular.module('iLayers') 11 | .directive('leaf', Leaf); 12 | 13 | Leaf.$inject = ['$location', 'commandService']; 14 | 15 | function Leaf($location, commandService) { 16 | return { 17 | templateUrl:'views/leaf.html', 18 | restrict: 'E', 19 | replace: true, 20 | controller: function($scope) { 21 | var locked = commandService.locked(), 22 | leaf = $scope.leaf; 23 | if (locked !== undefined && leaf.name === locked.name && leaf.tag === locked.tag) { 24 | $scope.lockParam = true; 25 | } 26 | }, 27 | link: function(scope, element) { 28 | 29 | scope.showCommands = function(repo, force) { 30 | var data = scope.graph; 31 | 32 | if (repo) { 33 | for (var i=0; i < data.length; i++) { 34 | if (data[i].repo.name === repo.name && data[i].repo.tag === repo.tag) { 35 | commandService.highlight(data[i].layers, force); 36 | break; 37 | } 38 | } 39 | } 40 | }; 41 | 42 | scope.applyLock = function(repo) { 43 | var lock = commandService.locked(); 44 | 45 | $('div.leaves section').removeClass('locked'); 46 | 47 | commandService.release(); 48 | 49 | if (lock !== undefined && lock.name === repo.name && lock.tag === repo.tag) { 50 | scope.lockParam = false; 51 | } else { 52 | scope.lockParam = true; 53 | scope.showCommands(repo); 54 | commandService.lock(repo); 55 | } 56 | }; 57 | 58 | scope.showCommands(commandService.locked(), scope.lockParam); 59 | } 60 | }; 61 | } 62 | })(); 63 | -------------------------------------------------------------------------------- /app/scripts/directives/loadingsrc.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:loadingSrc 7 | * @description 8 | * # loadingSrc 9 | */ 10 | angular.module('iLayers') 11 | .directive('loadingSrc', LoadingSrc); 12 | 13 | LoadingSrc.$inject = ['errorService']; 14 | 15 | function LoadingSrc(errorService) { 16 | return { 17 | restrict: 'A', 18 | link: function postLink(scope, element, attrs) { 19 | var img = new Image(); 20 | 21 | attrs.$observe('loadingSrc', function() { 22 | element[0].src = (attrs.loading) ? attrs.loading : '/images/loading.gif'; 23 | 24 | img.onload = function() { 25 | element[0].src = img.src; 26 | }; 27 | 28 | img.onerror = function() { 29 | errorService.notification('Unable to load badge.'); 30 | }; 31 | 32 | img.src = attrs.loadingSrc; 33 | }); 34 | } 35 | }; 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /app/scripts/directives/metrics.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .directive('metrics', Metrics); 6 | 7 | Metrics.$inject = ['$timeout']; 8 | 9 | function Metrics($timeout) { 10 | return { 11 | templateUrl: 'views/metrics.html', 12 | restrict: 'E', 13 | controller: function($scope) { 14 | $scope.metrics = { 15 | count: 0, 16 | size: 0, 17 | ave: 0, 18 | largest: 0 19 | }; 20 | }, 21 | link: function(scope) { 22 | 23 | scope.sequential = function(key, start, end, duration) { 24 | 25 | var range = end - start; 26 | var minTimer = 50; 27 | 28 | // calc step time to show all interediate values 29 | var stepTime = Math.abs(Math.floor(duration / range)); 30 | 31 | // never go below minTimer 32 | stepTime = Math.max(stepTime, minTimer); 33 | 34 | // get current time and calculate desired end time 35 | var startTime = new Date().getTime(); 36 | var endTime = startTime + duration; 37 | 38 | function run() { 39 | var now = new Date().getTime(); 40 | var remaining = Math.max((endTime - now) / duration, 0); 41 | var value = Math.round(end - (remaining * range)); 42 | scope.metrics[key] = value; 43 | if (value !== end) { 44 | $timeout(run, stepTime); 45 | } 46 | } 47 | 48 | run(); 49 | }; 50 | 51 | scope.calculateMetrics = function(data) { 52 | var count = 0, 53 | size = 0, 54 | ave = 0, 55 | largest = 0, 56 | cache = {}, 57 | layers = []; 58 | 59 | for (var z=0; z< data.length; z++) { 60 | angular.forEach(data[z].layers, function(l) { 61 | cache[l.id] = l; 62 | }); 63 | } 64 | 65 | for (var key in cache) { 66 | if (cache.hasOwnProperty(key)) { 67 | layers.push(cache[key]); 68 | } 69 | } 70 | 71 | for (var i=0; i < layers.length; i++) { 72 | count += 1; 73 | size += layers[i].Size; 74 | largest = Math.max(largest, layers[i].Size); 75 | ave = Math.floor(size / count); 76 | } 77 | 78 | // animate the numbers 79 | scope.sequential('count', scope.metrics.count, count, 600); 80 | scope.sequential('size', scope.metrics.size, size, 520); 81 | scope.sequential('ave', scope.metrics.ave, ave, 520); 82 | scope.sequential('largest', scope.metrics.largest, largest, 520); 83 | }; 84 | 85 | scope.$watch('graph', function() { 86 | if (scope.graph.length > 0) { 87 | scope.calculateMetrics(scope.graph); 88 | } 89 | }, true); 90 | 91 | scope.$watch('filters.image', function(filter) { 92 | if (scope.graph !== undefined) { 93 | var graphData = scope.applyFilters(scope.graph, filter); 94 | scope.calculateMetrics(graphData); 95 | } 96 | }); 97 | } 98 | }; 99 | } 100 | })(); 101 | -------------------------------------------------------------------------------- /app/scripts/directives/mobile.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:badge 7 | * @description 8 | * # mobile 9 | */ 10 | angular.module('iLayers').directive('mobile', Mobile); 11 | 12 | function Mobile() { 13 | return { 14 | templateUrl: 'views/mobile.html', 15 | restrict: 'E', 16 | replace: 'true', 17 | link: function($scope) { 18 | $scope.dismiss = function (){ 19 | $('.mobile').remove(); 20 | }; 21 | } 22 | }; 23 | } 24 | })(); 25 | -------------------------------------------------------------------------------- /app/scripts/directives/notification.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:notification 7 | * @description 8 | * # notification 9 | */ 10 | angular.module('iLayers') 11 | .directive('notification', Notification); 12 | 13 | function Notification() { 14 | return { 15 | template: '
{{ message }}
', 16 | restrict: 'E', 17 | replace: true, 18 | link: function postLink(scope, element) { 19 | scope.message = ''; 20 | 21 | element.hide(); 22 | 23 | scope.$on('notification', function(evt, data) { 24 | element.fadeIn(); 25 | scope.message = data.msg; 26 | scope.loading = false; 27 | $('body').addClass('error'); 28 | }); 29 | 30 | scope.dismiss = function() { 31 | element.fadeOut(400, function() { 32 | $('body').removeClass('error'); 33 | }); 34 | } 35 | } 36 | }; 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /app/scripts/directives/resetfield.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:resetField 7 | * @description 8 | * # resetField 9 | */ 10 | angular.module('iLayers') 11 | .directive('resetField', ResetField); 12 | 13 | ResetField.$inject = ['$timeout']; 14 | 15 | function ResetField($timeout) { 16 | return { 17 | restrict: 'A', 18 | scope: {}, 19 | require: 'ngModel', 20 | link: function postLink(scope, element, attrs, ctrl) { 21 | var resetElement = angular.element(''); 22 | 23 | element.after(resetElement); 24 | 25 | resetElement.bind('click', function() { 26 | ctrl.$setViewValue(''); 27 | ctrl.$render(); 28 | 29 | $timeout(function() { 30 | element[0].focus(); 31 | }, 0, false); 32 | }); 33 | 34 | element.bind('input focus', function() { 35 | if (!ctrl.$isEmpty(element.val())) { 36 | resetElement.removeClass('hidden'); 37 | } else { 38 | resetElement.addClass('hidden'); 39 | } 40 | scope.$apply(); 41 | }); 42 | } 43 | }; 44 | } 45 | })(); 46 | -------------------------------------------------------------------------------- /app/scripts/directives/sticky.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:sticky 7 | * @description 8 | * # sticky 9 | */ 10 | angular.module('iLayers') 11 | .directive('sticky', Sticky); 12 | 13 | function Sticky() { 14 | return { 15 | restrict: 'A', 16 | link: function postLink(scope, element, attrs) { 17 | var main = element.closest('main'), 18 | offset = (attrs.offset) ? attrs.offset : 0; 19 | 20 | main.bind('scroll', function() { 21 | if (main.scrollTop() >= offset) { 22 | element.addClass('sticky'); 23 | $('.matrix').css('padding-top', 160); 24 | } else { 25 | element.removeClass('sticky'); 26 | $('.matrix').css('padding-top', 0); 27 | } 28 | }); 29 | 30 | } 31 | }; 32 | } 33 | })(); 34 | -------------------------------------------------------------------------------- /app/scripts/directives/syncScroll.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc directive 6 | * @name iLayers.directive:sticky 7 | * @description 8 | * # horizonScroll 9 | */ 10 | angular.module('iLayers') 11 | .directive('syncScroll', SyncScroll 12 | ); 13 | 14 | function SyncScroll() { 15 | return { 16 | restrict: 'A', 17 | link: function postLink(scope, element, attrs) { 18 | var main = element.closest('main'), 19 | offset = attrs.offset ? attrs.offset : 0, 20 | syncTarget = attrs.syncTarget ? $(attrs.syncTarget) : $('body'); 21 | 22 | syncTarget.bind('scroll', function() { 23 | if ($('main').scrollTop() >= offset) { 24 | element.css('left', syncTarget.scrollLeft() * -1); 25 | } else { 26 | element.css('left', 0); 27 | } 28 | }); 29 | 30 | main.bind('scroll', function() { 31 | if ($('main').scrollTop() < offset) { 32 | element.css('left', 0); 33 | } else { 34 | element.css('left', syncTarget.scrollLeft() * -1); 35 | } 36 | }); 37 | } 38 | }; 39 | } 40 | })(); 41 | -------------------------------------------------------------------------------- /app/scripts/filters/size.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .filter('size', ['$sce', function($sce) { 6 | var bytesToSize = function(bytes) { 7 | var sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']; 8 | if (bytes === 1) { return '1 Byte'; } 9 | if (bytes === 0) { return '0 Bytes'; } 10 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000))); 11 | return Math.round(bytes / Math.pow(1000, i), 2) + ' ' + sizes[i] + ''; 12 | }; 13 | 14 | return function(input) { 15 | return $sce.trustAsHtml(bytesToSize(input)); 16 | }; 17 | }]); 18 | })(); 19 | -------------------------------------------------------------------------------- /app/scripts/services/commandservice.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .factory('commandService', CommandService); 6 | 7 | CommandService.$inject = ['$rootScope']; 8 | 9 | function CommandService($rootScope) { 10 | var locked; 11 | 12 | return { 13 | highlight: function (layers, force) { 14 | /*jshint camelcase: false */ 15 | var cmds = []; 16 | for(var i=0; i < layers.length; i++) { 17 | var cmd = layers[i].container_config.Cmd, 18 | size = layers[i].Size || 0, 19 | txt = (cmd) ? cmd.join(' ') : null; 20 | cmds.push(this.constructCommand(txt, size)); 21 | } 22 | 23 | if (locked === undefined || force === true) { 24 | $rootScope.$broadcast('command-change', { 'commands': cmds }); 25 | } 26 | }, 27 | 28 | constructCommand: function (cmd, size) { 29 | var nop = '(nop) '; 30 | 31 | if (cmd === null || cmd == '') { // jshint ignore:line 32 | if (size > 0) { 33 | cmd = 'unknown instruction'; 34 | } else { 35 | cmd = 'FROM scratch'; 36 | } 37 | } 38 | 39 | if (cmd !== undefined) { 40 | if (cmd.lastIndexOf(nop) > 0) { 41 | cmd = cmd.split(nop)[1]; 42 | } 43 | else { 44 | cmd = cmd.replace(/\/bin\/sh\/*\s-c/g, 'RUN'); 45 | } 46 | cmd = cmd.replace(/(map|\/tcp:{}|\[|\]'?|\))/g, '').trim(); 47 | } 48 | else { 49 | cmd = ''; 50 | } 51 | return cmd; 52 | }, 53 | 54 | clear: function() { 55 | $rootScope.$broadcast('command-change', { 'commands': [] }); 56 | }, 57 | 58 | locked: function() { 59 | return locked; 60 | }, 61 | 62 | lock: function(image) { 63 | if (image !== undefined) { 64 | locked = image; 65 | } 66 | 67 | $rootScope.$broadcast('lock-image', { 'image': locked }); 68 | return locked; 69 | }, 70 | 71 | release: function() { 72 | locked = undefined; 73 | $rootScope.$broadcast('lock-image', { 'image': locked }); 74 | } 75 | }; 76 | } 77 | })(); 78 | -------------------------------------------------------------------------------- /app/scripts/services/errorinterceptor.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc service 6 | * @name iLayers.errorInterceptor 7 | * @description 8 | * # errorInterceptor 9 | * Factory in the iLayers. 10 | */ 11 | angular.module('iLayers') 12 | .factory('errorInterceptor', ErrorInterceptor); 13 | 14 | ErrorInterceptor.$inject = ['$q', 'errorService']; 15 | 16 | function ErrorInterceptor($q, errorService) { 17 | return { 18 | response: function(response) { 19 | return response; 20 | }, 21 | responseError: function(response) { 22 | var msg = 'Unable to communicate to ImageLayers Services'; 23 | 24 | if (response.data) { 25 | msg = response.data; 26 | } 27 | errorService.error(msg); 28 | return $q.reject(response); 29 | } 30 | }; 31 | } 32 | })(); -------------------------------------------------------------------------------- /app/scripts/services/errorservice.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc service 6 | * @name iLayers.errorService 7 | * @description 8 | * # errorService 9 | * Factory in the iLayers. 10 | */ 11 | angular.module('iLayers') 12 | .factory('errorService', ErrorService); 13 | 14 | ErrorService.$inject = ['$rootScope']; 15 | 16 | function ErrorService($rootScope) { 17 | var broadcastError = function(error) { 18 | $rootScope.$broadcast('notification', error); 19 | }; 20 | 21 | return { 22 | error: function (msg) { 23 | console.log('Error: ', msg); 24 | broadcastError({ type: 'error', msg: msg }); 25 | }, 26 | notification: function(msg) { 27 | broadcastError({ type: 'notification', msg: msg }); 28 | } 29 | }; 30 | } 31 | })(); 32 | -------------------------------------------------------------------------------- /app/scripts/services/gridservice.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .factory('gridService', GridService); 6 | function GridService() { 7 | var inventory = {}, 8 | matrix = {}; 9 | 10 | var findLongest = function(sortedImages) { 11 | if (sortedImages.length === 0) { 12 | return 0; 13 | } 14 | 15 | return sortedImages[0].layers.length; 16 | }; 17 | 18 | var findLayerIndex = function(layerId, layers) { 19 | var idx = -1; 20 | 21 | for (var i = 0; i < layers.length; i++) { 22 | if (layers[i].id === layerId) { 23 | idx = i; 24 | } 25 | } 26 | 27 | return idx; 28 | }; 29 | 30 | var zeroMatrix = function(rows, cols) { 31 | var m = []; 32 | 33 | for(var i=0; i < cols; i++) { 34 | m[i] = new Array(rows); 35 | for (var r=0; r < rows; r++) { 36 | m[i][r] = { 37 | 'layer': { 38 | 'id': 'empty' 39 | } 40 | }; 41 | } 42 | } 43 | 44 | return m; 45 | }; 46 | 47 | var changeInventoryCounts = function(images, layer, col) { 48 | var location = inventory[layer.id], 49 | pIdx = findLayerIndex(layer.id, location.image.layers), 50 | parent = (pIdx !== -1) ? location.image.layers[pIdx].parent : ''; 51 | 52 | location.count = location.count + 1; 53 | matrix[col][location.row] = { 'type': 'box', 'layer': layer }; 54 | if (parent !== '') { 55 | var l = inventory[parent], 56 | idx = findLayerIndex(parent, l.image.layers); 57 | 58 | if (idx !== -1) { 59 | changeInventoryCounts(images,l.image.layers[idx], col); 60 | } 61 | } 62 | }; 63 | 64 | var initializeInventory = function(list) { 65 | var inv = {}, 66 | images = sortImages(list).reverse(); 67 | 68 | for (var i=0; i < images.length; i++) { 69 | for (var l=0; l < images[i].layers.length; l++) { 70 | var layer = images[i].layers[l]; 71 | if (inv[layer.id] === undefined) { 72 | inv[layer.id] = { 'image': images[i], 'row': 0, 'count': 0 }; 73 | } 74 | } 75 | } 76 | 77 | return inv; 78 | }; 79 | 80 | var sortImages = function(images) { 81 | var compare = function(a,b) { 82 | 83 | if (a.layers.length < b.layers.length) { 84 | return 1; 85 | } 86 | if (a.layers.length > b.layers.length) { 87 | return -1; 88 | } 89 | return 0; 90 | }; 91 | 92 | if (images.length !== 0) { 93 | return images.sort(compare); 94 | } else { 95 | return images; 96 | } 97 | }; 98 | 99 | var sortByLayerCohesion = function(groups, row) { 100 | var retval = []; 101 | 102 | angular.forEach(groups, function(group) { 103 | var set = []; 104 | 105 | angular.forEach(group, function(image) { 106 | // Go through all the columns in the group // 107 | for (var g=0; g < image.length; g++) { 108 | var col = image[g], 109 | subject = {}, 110 | found = false; 111 | 112 | if (set.length === 0) { 113 | set.push([col]); 114 | } else { 115 | subject = col[row]; 116 | // look for match in temporary set 117 | for (var i=0; i < set.length; i++) { 118 | if (subject.layer.id !== 'empty' && subject.layer.id === set[i][0][row].layer.id) 119 | { 120 | set[i].push(col); 121 | found = true; 122 | } 123 | } 124 | if (!found) { 125 | set.push([col]); 126 | } 127 | } 128 | } 129 | }); 130 | retval.push(set); 131 | }); 132 | 133 | return retval; 134 | }; 135 | 136 | var groupDependent = function(matrix) { 137 | var groups = [], 138 | merged = [], 139 | sets = [], 140 | rows = (matrix.length) ? matrix[0].length : 0; 141 | 142 | groups.push([]); 143 | groups[0].push(matrix); 144 | 145 | for (var row=0; row < rows; row++) { 146 | groups = sortByLayerCohesion(groups, row); 147 | } 148 | 149 | // merge groups 150 | sets = sets.concat.apply(sets, groups); 151 | merged = merged.concat.apply(merged, sets); 152 | 153 | return merged; 154 | }; 155 | 156 | var buildMatrix = function(sortedImages) { 157 | for (var i=0; i < sortedImages.length; i++) { 158 | for (var j= sortedImages[i].layers.length-1; j >= 0; j--) { 159 | var layer = sortedImages[i].layers[j]; 160 | 161 | if (inventory[layer.id].count > 0) { 162 | j = changeInventoryCounts(sortedImages, layer, i); 163 | } else { 164 | inventory[layer.id].row = j; 165 | inventory[layer.id].count = 1; 166 | matrix[i][j] = { 'layer': layer }; 167 | } 168 | } 169 | } 170 | 171 | return { 172 | inventory: inventory, 173 | map: groupDependent(matrix) 174 | }; 175 | 176 | }; 177 | 178 | return { 179 | buildGrid: function(images) { 180 | var sortedImages = [], 181 | sanitizedList = [], 182 | long = 0; 183 | 184 | angular.forEach(images, function(image) { 185 | if (image.layers.length > 0) { 186 | sanitizedList.push(image); 187 | } 188 | }); 189 | 190 | inventory = initializeInventory(sanitizedList); 191 | sortedImages = sortImages(sanitizedList); 192 | 193 | long = findLongest(sortedImages); 194 | matrix = zeroMatrix(long, sortedImages.length); 195 | 196 | return { 197 | rows: findLongest(sortedImages), 198 | cols: sanitizedList.length, 199 | matrix: buildMatrix(sortedImages) 200 | }; 201 | }, 202 | 203 | inventory: function(id) { 204 | return inventory[id]; 205 | }, 206 | 207 | matrix: function() { 208 | return matrix; 209 | }, 210 | 211 | findLeaves: function(grid) { 212 | /*jshint camelcase: false */ 213 | var leaves = []; 214 | 215 | for (var c=0; c < grid.cols; c++) { 216 | for (var l=grid.rows -1; l >= 0; l--) { 217 | var id = grid.matrix.map[c][l].layer.id, 218 | repo = {}; 219 | 220 | if (id !== 'empty') { 221 | angular.copy(grid.matrix.inventory[id].image.repo, repo); 222 | repo.identity = repo.name + '::' + repo.tag + Math.floor(Math.random() * 10000); 223 | var link = repo.name.lastIndexOf('/') < 0 ? '_/' + repo.name : 'r/' + repo.name; 224 | repo.hub_link = 'https://hub.docker.com/' + link; 225 | leaves.push(repo); 226 | break; 227 | } 228 | } 229 | } 230 | 231 | return leaves; 232 | } 233 | }; 234 | } 235 | })(); 236 | -------------------------------------------------------------------------------- /app/scripts/services/registryservice.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | angular.module('iLayers') 5 | .factory('registryService', RegistryService); 6 | 7 | RegistryService.$inject = ['$http', '$q', 'errorService', 'ENV']; 8 | 9 | function RegistryService($http, $q, errorService, ENV) { 10 | 11 | function retryQuery(queryFn, maxRetry) { 12 | var callResults = $q.defer(), 13 | count = 1; 14 | 15 | function doCall() { 16 | queryFn().then( 17 | function(response) { 18 | callResults.resolve(response); 19 | }, 20 | function(response) { 21 | if (count < maxRetry) { 22 | count++; 23 | // Backoff timer 0 - 750ms 24 | setTimeout(doCall, (750 * Math.random())); 25 | } else { 26 | response.retryLimit = true; 27 | callResults.reject(response); 28 | } 29 | }); 30 | } 31 | 32 | doCall(); 33 | return callResults.promise; 34 | } 35 | 36 | return { 37 | inspect: function (list) { 38 | function doInspection() { 39 | return $http.post((ENV.apiEndpoint || '') + '/registry/analyze', { 'repos': list }) 40 | .then(function(response) { 41 | var images = response.data, 42 | list = [], 43 | missing = []; 44 | 45 | angular.forEach(images, function(image) { 46 | if (image.layers !== null) { 47 | image.layers = image.layers.reverse(); 48 | list.push(image); 49 | } else { 50 | missing.push(image.repo.name + ':' + image.repo.tag); 51 | } 52 | }); 53 | 54 | if (missing.length >0) { 55 | errorService.notification('The following images could not be found: ' + missing.join(' ')); 56 | } 57 | 58 | response.data = list; 59 | return response; 60 | }); 61 | } 62 | 63 | return retryQuery(doInspection, 3); 64 | }, 65 | search: function(name) { 66 | return retryQuery(function() { 67 | return $http.get((ENV.apiEndpoint || '') + '/registry/search?name='+name); 68 | }, 3); 69 | }, 70 | fetchTags: function(name) { 71 | return retryQuery(function() { 72 | return $http.get((ENV.apiEndpoint || '') + '/registry/images/'+name+'/tags'); 73 | }, 3); 74 | } 75 | }; 76 | } 77 | })(); 78 | -------------------------------------------------------------------------------- /app/styles/_icons.scss: -------------------------------------------------------------------------------- 1 | @import '_mixins'; 2 | 3 | $icons: ( 4 | plus: 0, 5 | minus: 1, 6 | x: 2, 7 | sprocket: 3, 8 | arrowloop: 4, 9 | checkmark: 5, 10 | document: 6, 11 | magnifying-glass: 7, 12 | pencil: 8, 13 | outbox: 9, 14 | grid: 10, 15 | envelope: 11, 16 | list: 12, 17 | folder: 13, 18 | menu: 14, 19 | house: 15, 20 | star: 16, 21 | thin-arrow-right: 17, 22 | thick-arrow-right: 18, 23 | magnifying-glass-plus: 19, 24 | magnifying-glass-minus: 20, 25 | comment-box: 21, 26 | happy-face: 22, 27 | sad-face: 23, 28 | copy: 24, 29 | twitter: 25, 30 | linkedin: 26, 31 | facebook: 27, 32 | link: 28, 33 | docker-blocks: 29, 34 | disks: 30, 35 | speedometer: 31, 36 | thin-arrow-down: 32, 37 | thick-arrow-down: 33, 38 | thin-arrow-left: 34, 39 | thick-arrow-left: 35, 40 | thin-arrow-up: 36, 41 | thick-arrow-up: 37, 42 | github: 38, 43 | google_plus: 39 44 | ); 45 | 46 | 47 | @mixin icon-base { 48 | background-image: url('../images/icon_sprite.svg'); 49 | background-repeat: no-repeat; 50 | width: 10px; 51 | height: 10px; 52 | @include background-size(10px, 1000px); 53 | } 54 | 55 | @mixin icon-xlarge { 56 | width: 25px; 57 | height: 25px; 58 | @include background-size(25px, 2500px); 59 | } 60 | 61 | @mixin icon-large { 62 | width: 20px; 63 | height: 20px; 64 | @include background-size(20px, 2000px); 65 | } 66 | 67 | @mixin icon-medium { 68 | width: 15px; 69 | height: 15px; 70 | @include background-size(15px, 1500px); 71 | } 72 | 73 | @mixin icon-grey { 74 | background-image: url('../images/icon_sprite.svg'); 75 | } 76 | 77 | @mixin icon-light-grey { 78 | background-image: url('../images/icon_sprite_light_grey.svg'); 79 | } 80 | 81 | @mixin icon-white { 82 | background-image: url('../images/icon_sprite_white.svg'); 83 | } 84 | 85 | @mixin icon-red { 86 | background-image: url('../images/icon_sprite_red.svg'); 87 | } 88 | 89 | @each $icon, $position in $icons { 90 | .icon-#{$icon} { 91 | @include icon-base; 92 | background-position: 0 -#{($position * 20)}px; 93 | } 94 | 95 | .icon-#{$icon}-medium { 96 | @include icon-base; 97 | @include icon-medium; 98 | background-position: 0 -#{($position * 30)}px; 99 | } 100 | 101 | .icon-#{$icon}-large { 102 | @include icon-base; 103 | @include icon-large; 104 | background-position: 0 -#{($position * 40)}px; 105 | } 106 | 107 | .icon-#{$icon}-xlarge { 108 | @include icon-base; 109 | @include icon-xlarge; 110 | background-position: 0 -#{($position * 50)}px; 111 | } 112 | 113 | .icon-#{$icon}-for-button { 114 | position: relative; 115 | 116 | &:after { 117 | @extend .icon-#{$icon}-medium; 118 | @include icon-white; 119 | content: ''; 120 | position: absolute; 121 | left: 12px; 122 | top: 12px; 123 | display: block; 124 | height: 15px; 125 | width: 15px; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin background-size($width, $height) { 2 | -webkit-background-size: $width $height; 3 | -o-background-size: $width $height; 4 | -moz-background-size: $width $height; 5 | background-size: $width $height; 6 | } 7 | 8 | @mixin box-shadow($top, $left, $blur, $color, $inset:"") { 9 | -webkit-box-shadow:$top $left $blur $color #{$inset}; 10 | -moz-box-shadow:$top $left $blur $color #{$inset}; 11 | box-shadow:$top $left $blur $color #{$inset}; 12 | } 13 | 14 | @mixin border-radius($radius) { 15 | -webkit-border-radius: #{$radius}; 16 | -moz-border-radius: #{$radius}; 17 | border-radius: #{$radius}; 18 | } 19 | 20 | @mixin border-box { 21 | -moz-box-sizing: border-box; 22 | -webkit-box-sizing: border-box; 23 | box-sizing: border-box; 24 | } 25 | -------------------------------------------------------------------------------- /app/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | @import '_mixins'; 2 | 3 | html, body, div, span, applet, object, iframe, 4 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 5 | a, abbr, acronym, address, big, cite, code, 6 | del, dfn, em, img, ins, kbd, q, s, samp, 7 | small, strike, strong, sub, sup, tt, var, 8 | b, u, i, center, 9 | dl, dt, dd, ol, ul, li, 10 | fieldset, form, label, legend, 11 | table, caption, tbody, tfoot, thead, tr, th, td, 12 | article, aside, canvas, details, embed, 13 | figure, figcaption, footer, header, hgroup, 14 | menu, nav, output, ruby, section, summary, 15 | time, mark, audio, video { 16 | margin: 0; 17 | padding: 0; 18 | border: 0; 19 | } 20 | 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | 27 | body { 28 | line-height: 1; 29 | } 30 | 31 | ol, ul { 32 | list-style: none; 33 | } 34 | 35 | table { 36 | border-collapse: collapse; 37 | border-spacing: 0; 38 | } 39 | 40 | * { 41 | @include border-box; 42 | } 43 | 44 | table, thead, tbody, tfoot, tr, td { 45 | box-sizing: content-box; 46 | } 47 | 48 | .clear { 49 | clear: both; 50 | } -------------------------------------------------------------------------------- /app/styles/animations.scss: -------------------------------------------------------------------------------- 1 | .repeat-animation.ng-enter-stagger, 2 | .repeat-animation.ng-move-stagger { 3 | /* 200ms will be applied between each sucessive enter operation */ 4 | -webkit-transition-delay:0.015s; 5 | transition-delay:0.015s; 6 | } 7 | 8 | .repeat-animation.ng-enter, 9 | .repeat-animation.ng-leave, 10 | .repeat-animation.ng-move { 11 | -webkit-transition:0.22s linear all; 12 | transition:0.22s linear all; 13 | } 14 | 15 | .repeat-animation.ng-leave.ng-leave-active, 16 | .repeat-animation.ng-enter, 17 | .repeat-animation.ng-move { 18 | -webkit-transition:0.22 linear all; 19 | transition:0.22s linear all; 20 | 21 | opacity:0; 22 | line-height:0; 23 | } 24 | 25 | .repeat-animation.ng-leave, 26 | .repeat-animation.ng-move.ng-move-active, 27 | .repeat-animation.ng-enter.ng-enter-active { 28 | opacity:1; 29 | line-height:20px; 30 | } 31 | 32 | /* Dialog Animations */ 33 | @-webkit-keyframes ngdialog-fadeout { 34 | 0% { 35 | opacity: 1; 36 | } 37 | 38 | 100% { 39 | opacity: 0; 40 | } 41 | } 42 | 43 | @keyframes ngdialog-fadeout { 44 | 0% { 45 | opacity: 1; 46 | } 47 | 48 | 100% { 49 | opacity: 0; 50 | } 51 | } 52 | 53 | @-webkit-keyframes ngdialog-fadein { 54 | 0% { 55 | opacity: 0; 56 | } 57 | 58 | 100% { 59 | opacity: 1; 60 | } 61 | } 62 | 63 | @keyframes ngdialog-fadein { 64 | 0% { 65 | opacity: 0; 66 | } 67 | 68 | 100% { 69 | opacity: 1; 70 | } 71 | } 72 | 73 | @-webkit-keyframes ngdialog-flyin { 74 | 0% { 75 | opacity: 0; 76 | -webkit-transform: translateY(-40px); 77 | transform: translateY(-40px); 78 | } 79 | 80 | 100% { 81 | opacity: 1; 82 | -webkit-transform: translateY(0); 83 | transform: translateY(0); 84 | } 85 | } 86 | 87 | @keyframes ngdialog-flyin { 88 | 0% { 89 | opacity: 0; 90 | -webkit-transform: translateY(-40px); 91 | -ms-transform: translateY(-40px); 92 | transform: translateY(-40px); 93 | } 94 | 95 | 100% { 96 | opacity: 1; 97 | -webkit-transform: translateY(0); 98 | -ms-transform: translateY(0); 99 | transform: translateY(0); 100 | } 101 | } 102 | 103 | @-webkit-keyframes ngdialog-flyout { 104 | 0% { 105 | opacity: 1; 106 | -webkit-transform: translateY(0); 107 | transform: translateY(0); 108 | } 109 | 110 | 100% { 111 | opacity: 0; 112 | -webkit-transform: translateY(-40px); 113 | transform: translateY(-40px); 114 | } 115 | } 116 | 117 | @keyframes ngdialog-flyout { 118 | 0% { 119 | opacity: 1; 120 | -webkit-transform: translateY(0); 121 | -ms-transform: translateY(0); 122 | transform: translateY(0); 123 | } 124 | 125 | 100% { 126 | opacity: 0; 127 | -webkit-transform: translateY(-40px); 128 | -ms-transform: translateY(-40px); 129 | transform: translateY(-40px); 130 | } 131 | } 132 | 133 | //loading indicator 134 | svg { 135 | display: block; 136 | width: 200px; 137 | margin: 40px auto 0; 138 | } 139 | 140 | @keyframes stack { 141 | 0% { 142 | transform: translateY(0px); 143 | opacity: .75; 144 | } 145 | 25% { 146 | transform: translateY(20px); 147 | opacity: .9; 148 | } 149 | 100% { 150 | opacity: .75; 151 | } 152 | } 153 | 154 | @-webkit-keyframes stack { 155 | 0% { 156 | transform: translateY(0px); 157 | -webkit-transform: translateY(0px); 158 | } 159 | 25% { 160 | transform: translateY(20px); 161 | -webkit-transform: translateY(20px); 162 | } 163 | } 164 | 165 | .red { 166 | animation: stack 2s linear infinite; 167 | -webkit-animation: stack 2s linear infinite; 168 | } 169 | .blue { 170 | animation: stack 2s linear 500ms infinite; 171 | -webkit-animation: stack 2s linear 500ms infinite; 172 | } 173 | .orange { 174 | animation: stack 2s linear 1000ms infinite; 175 | -webkit-animation: stack 2s linear 1000ms infinite; 176 | } 177 | .green { 178 | animation: stack 2s linear 1500ms infinite; 179 | -webkit-animation: stack 2s linear 1500ms infinite; 180 | } 181 | -------------------------------------------------------------------------------- /app/styles/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree imagelayers 13 | *= require_directory imagelayers 14 | */ 15 | 16 | //* { 17 | // outline: 1px solid #d95555 18 | //} 19 | 20 | @import "header"; 21 | @import "grid"; 22 | @import "dialog"; 23 | @import "animations"; 24 | @import "main"; 25 | 26 | -------------------------------------------------------------------------------- /app/styles/error.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | 3 | body.error-page { 4 | width: 100%; 5 | font-family: Helvetica, Arial, sans-serif; 6 | 7 | main { 8 | width: 100%; 9 | max-width: 800px; 10 | margin: 10% auto 0; 11 | p { 12 | margin-top: 10%; 13 | text-align: center; 14 | font-weight: bold; 15 | color: #666; 16 | font-size: 16px; 17 | } 18 | a { 19 | text-decoration: none; 20 | font-style: normal; 21 | color: #a0a0a0; 22 | } 23 | } 24 | h1.logo { 25 | height: 150px; 26 | width: auto; 27 | text-indent: -9999px; 28 | color: transparent; 29 | background: url('images/logo_image_layers.svg') no-repeat 10px center; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/styles/grid.scss: -------------------------------------------------------------------------------- 1 | /* Graph */ 2 | 3 | main { 4 | margin: 0; 5 | height: auto; 6 | position: absolute; 7 | top: 0; 8 | left: 0; 9 | bottom: 122px; 10 | right: 0; 11 | padding-bottom: 5px; 12 | overflow: auto; 13 | -webkit-touch-callout: none; 14 | -webkit-user-select: none; 15 | -khtml-user-select: none; 16 | -moz-user-select: none; 17 | -ms-user-select: none; 18 | -o-user-select: none; 19 | user-select: none; 20 | 21 | h2 { 22 | margin-top: 0; 23 | margin-bottom: .5em; 24 | font-size: 30px; 25 | } 26 | 27 | small { 28 | display: block; 29 | } 30 | 31 | div.info { 32 | width: 90%; 33 | margin: 50px auto 0; 34 | text-align: center; 35 | color: #999; 36 | font-weight: bold; 37 | h3 { 38 | font-size: 24px; 39 | color: #333; 40 | } 41 | p { 42 | max-width: 500px; 43 | margin: 40px auto; 44 | color: #666; 45 | } 46 | } 47 | 48 | div.leaves { 49 | padding-top: 28px; 50 | padding-left: 10px; 51 | white-space: nowrap; 52 | overflow: hidden; 53 | box-sizing: content-box; 54 | border-bottom: 1px solid #b3b3b3; 55 | background-color: #d8d8d8; 56 | position: relative; 57 | z-index: 9; 58 | min-width: 96.5%; 59 | margin-right: 0; 60 | 61 | &.sticky { 62 | position: fixed; 63 | top: 59px; 64 | width: 100%; 65 | } 66 | 67 | a.hub-link { 68 | color: #666; 69 | margin-bottom: 6px; 70 | display: block; 71 | &:hover { 72 | color: #229FE5; 73 | } 74 | } 75 | 76 | section { 77 | width: 180px; 78 | margin-left: 20px; 79 | padding: 8px; 80 | font-size: 0.85em; 81 | text-align: center; 82 | background: #c5c5c5; 83 | color: #2d2d2d; 84 | display: inline-block; 85 | white-space: nowrap; 86 | 87 | &:hover { 88 | cursor: pointer; 89 | background: #fff; 90 | } 91 | 92 | &.locked { 93 | background: #fff; 94 | } 95 | 96 | .name { 97 | white-space: normal; 98 | word-wrap: break-word; 99 | max-height: 2.5em; 100 | overflow: hidden; 101 | } 102 | 103 | .size { 104 | font-size: 1.3em; 105 | font-weight: bold; 106 | margin-bottom: 3px; 107 | } 108 | 109 | .count { 110 | color: #999; 111 | font-size: 0.9em; 112 | } 113 | } 114 | } 115 | 116 | .matrix { 117 | } 118 | 119 | section.graph { 120 | position: relative; 121 | width: 100%; 122 | overflow: auto; 123 | 124 | .noop { 125 | width: 0; 126 | height: 0; 127 | display: none; 128 | } 129 | 130 | .box { 131 | box-sizing: content-box; 132 | padding: 10px; 133 | float: left; 134 | background: #fff; 135 | color: #222; 136 | margin: 10px; 137 | position: relative; 138 | left: 20px; 139 | font-size: 0.9em; 140 | 141 | &.locked { 142 | border: 10px solid #e3e2a6; 143 | margin: 0; 144 | } 145 | 146 | &.cat5 { 147 | background: #aaa; 148 | color: #444; 149 | } 150 | 151 | &.cat4 { 152 | background: #fff; 153 | color: #444; 154 | } 155 | 156 | &.cat3 { 157 | background: #699C48; 158 | color: #fff; 159 | } 160 | 161 | &.cat2 { 162 | background: #229FE5; 163 | color: #fff; 164 | } 165 | 166 | &.cat1 { 167 | background: #186D95; 168 | color: #fff; 169 | } 170 | 171 | &.empty { 172 | background: transparent; 173 | width: 160px; 174 | } 175 | 176 | > span { 177 | display: block; 178 | margin: 0 auto; 179 | text-align: center; 180 | 181 | &.size { 182 | font-weight: bold; 183 | margin-top: 5px; 184 | } 185 | &.command { 186 | text-overflow: ellipsis; 187 | white-space: nowrap; 188 | overflow: hidden; 189 | font-size: 12px; 190 | } 191 | } 192 | 193 | &:not(.empty):before { 194 | content: ''; 195 | display: block; 196 | height: 20px; 197 | width: 100%; 198 | position: absolute; 199 | left: -10px; 200 | top: -20px; 201 | background-image: url('../images/grid_line.png'); 202 | } 203 | } 204 | 205 | @for $i from 1 through 50 { 206 | .box-#{$i} { width: 160px * $i + ($i - 1) *40; } 207 | } 208 | } 209 | 210 | .layer { 211 | text-align: center; 212 | vertical-align: middle; 213 | font-family: Consolas, Monaco, "Courier New", Courier, monospace; 214 | background-color: #cccccc; 215 | font-size: .75em; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /app/styles/header.scss: -------------------------------------------------------------------------------- 1 | @import '_icons'; 2 | 3 | 4 | /* About Dropdown */ 5 | div.about { 6 | display: none; 7 | z-index: 300; 8 | position: absolute; 9 | top: 59px; 10 | left: 190px; 11 | width: 722px; 12 | background-color: #fff; 13 | opacity: 1; 14 | padding: 20px; 15 | border: 1px solid #999999; 16 | border-top: none; 17 | font-size: .8em; 18 | line-height: 1.25em; 19 | color: #666666; 20 | 21 | header { 22 | margin-bottom: 15px; 23 | a { 24 | height: 45px; 25 | } 26 | } 27 | 28 | a { 29 | font-weight: normal; 30 | color: #9c9c9c; 31 | text-decoration: none; 32 | &:hover { 33 | color: #666; 34 | text-decoration: underline; 35 | } 36 | &.github { 37 | margin-right: 8px; 38 | border-right: 1px solid #9c9c9c; 39 | padding-right: 10px; 40 | } 41 | } 42 | 43 | > p { 44 | margin-bottom: 15px; 45 | &.info { 46 | font-weight: bold; 47 | margin-bottom: 40px; 48 | } 49 | } 50 | 51 | ul { 52 | width: 100%; 53 | overflow: auto; 54 | border-top: 1px solid #ccc; 55 | border-bottom: 1px solid #ccc; 56 | margin: 20px 0; 57 | padding: 20px 0; 58 | li { 59 | width: 72%; 60 | float: left; 61 | margin-top: 20px; 62 | margin-right: 2%; 63 | &:last-child { 64 | margin-right: 0; 65 | } 66 | > a { 67 | background: #f1f1f1; 68 | padding: 10px; 69 | text-decoration: none; 70 | display: block; 71 | font-size: .9em; 72 | 73 | span { 74 | color: #2d2d2d; 75 | text-decoration: underline; 76 | } 77 | 78 | &:hover { 79 | text-decoration: none; 80 | background: #e1e1e1; 81 | span { 82 | text-decoration: none; 83 | } 84 | } 85 | 86 | h4 { 87 | color: #666; 88 | margin: 15px 0; 89 | } 90 | 91 | [class*='-logo'] { 92 | padding: 0 0 60px 0; 93 | } 94 | 95 | .pmx-logo { 96 | background: url('../images/panamax_id_02.svg') -135px -310px no-repeat; 97 | background-size: 245%; 98 | } 99 | 100 | .lorry-logo { 101 | background: url('../images/logo_lorry.svg') -5px 0 no-repeat; 102 | } 103 | 104 | .dray-logo { 105 | background: url('../images/logo_dray_long.svg') -275px -415px no-repeat; 106 | background-size: 350%; 107 | } 108 | } 109 | } 110 | } 111 | 112 | footer { 113 | display: block; 114 | position: relative; 115 | height: auto; 116 | background: none; 117 | font-weight: bold; 118 | font-size: 1.0em; 119 | padding-bottom: 0; 120 | 121 | a { 122 | color: #666; 123 | text-decoration: none; 124 | font-weight: bold; 125 | &:hover { 126 | color: #333; 127 | text-decoration: underline; 128 | } 129 | } 130 | } 131 | } 132 | 133 | /* Header and Nav*/ 134 | main { 135 | > header { 136 | width: 100%; 137 | min-width: 1000px; 138 | background: #d8d8d8; 139 | height: 59px; 140 | top: 0; 141 | 142 | h1.imagelayers-logo { 143 | float: left; 144 | height: 100%; 145 | a { 146 | margin-top: 5px; 147 | height: 50px; 148 | } 149 | } 150 | 151 | nav.ctl { 152 | width: 58px; 153 | float: left; 154 | position: relative; 155 | margin-left: 10px; 156 | height: 100%; 157 | a { 158 | color: #888; 159 | font-size: .8em; 160 | text-decoration: none; 161 | padding-top: 23px; 162 | display: block; 163 | height: 59px; 164 | &:after { 165 | @extend .icon-thin-arrow-down; 166 | @include icon-light-grey; 167 | content: ''; 168 | display: block; 169 | position: absolute; 170 | top: 26px; 171 | right: 5px; 172 | width: 16px; 173 | height: 16px; 174 | } 175 | } 176 | } 177 | 178 | .ctl-cloud { 179 | background-color: #0040D0; 180 | cite { 181 | float: right; 182 | &.ctl-logo { 183 | display: block; 184 | a { 185 | height: 50px; 186 | } 187 | } 188 | 189 | &.cloud { 190 | width: 150px; 191 | padding-left: 20px; 192 | border-left: 1px solid #a0a0a0; 193 | margin: 13px 10px 0 20px; 194 | a { 195 | text-decoration: none; 196 | font-style: normal; 197 | color: #a0a0a0; 198 | font-size: 14px; 199 | span { 200 | position: relative; 201 | top: -2px; 202 | display: inline-block; 203 | color: #777; 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'reset'; 2 | @import '_icons'; 3 | 4 | body { 5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | background-color: #d8d8d8; 7 | overflow-x: hidden; 8 | } 9 | // base project logo 10 | .imagelayers-logo { 11 | width: 190px; 12 | padding: 0; 13 | margin: 0; 14 | a { 15 | display: block; 16 | background: url('../images/logo_image_layers.svg') no-repeat 10px center; 17 | text-indent: -9999px; 18 | color: transparent; 19 | } 20 | } 21 | 22 | // base microscaling logo 23 | .ctl-logo { 24 | width: 140px; 25 | padding: 0; 26 | margin: 0; 27 | a { 28 | display: block; 29 | background: url('../images/microscaling_logo.svg') 0 15px no-repeat; 30 | background-size: 140px 30px; 31 | text-indent: -9999px; 32 | color: transparent; 33 | } 34 | } 35 | 36 | #error { 37 | background: rgb(217,0,0); 38 | margin: 10px 2%; 39 | color: #fff; 40 | padding: 10px; 41 | width: 95%; 42 | position: relative; 43 | 44 | div.dismiss { 45 | width: 18px; 46 | height: 18px; 47 | position: absolute; 48 | right: 5px; 49 | top: 12px; 50 | 51 | &:hover { 52 | cursor: pointer; 53 | } 54 | 55 | &:after { 56 | @extend .icon-x-medium; 57 | content: ' '; 58 | display: block; 59 | width: 18px; 60 | height: 18px; 61 | position: absolute; 62 | top: 0; 63 | left: 0; 64 | @include icon-white; 65 | } 66 | } 67 | } 68 | 69 | .icon-button { 70 | margin-top: 18px; 71 | float: left; 72 | position: relative; 73 | text-decoration: underline; 74 | padding-left: 30px; 75 | margin-left: 20px; 76 | &:hover { 77 | text-decoration: none; 78 | cursor: pointer; 79 | } 80 | 81 | &:before, &:after { 82 | content: ''; 83 | width: 24px; 84 | height: 24px; 85 | position: absolute; 86 | display: block; 87 | left: 0; 88 | top: -4px; 89 | } 90 | 91 | &:before { 92 | border-radius: 12px; 93 | } 94 | 95 | &:after { 96 | background-size: 12px 12px; 97 | } 98 | 99 | &.plus { 100 | &:before { 101 | background: #91B748; 102 | } 103 | &:after { 104 | background: url('../images/plus-white.svg') 45% 45% no-repeat; 105 | } 106 | } 107 | 108 | &.copy { 109 | cursor: pointer; 110 | &:before { 111 | background: #AAAAAA; 112 | } 113 | &:after { 114 | @extend .icon-checkmark; 115 | top: 1px; 116 | left: 4px; 117 | height: 15px; 118 | width: 15px; 119 | background-size: 15px 1500px; 120 | background-position: 0 -150px; 121 | background-image: url('../images/icon_sprite_white.svg') !important; 122 | } 123 | } 124 | &.copied:before { 125 | background: #91B748; 126 | } 127 | } 128 | 129 | section input[type="text"] { 130 | width: 30%; 131 | float: right; 132 | margin-top: 10px; 133 | margin-right: 20px; 134 | padding: 10px; 135 | font-size: 14px; 136 | font-style: italic; 137 | } 138 | 139 | /* Metrics */ 140 | section.metrics { 141 | width: 100%; 142 | min-width: 1225px; 143 | background: #2d2d2d; 144 | padding: 10px 20px; 145 | position: relative; 146 | z-index: 99; 147 | 148 | li { 149 | display: inline-block; 150 | width: 239px; 151 | height: 51px; 152 | border-right: 1px solid #4b4b4b; 153 | 154 | &:last-child { 155 | border-right: none; 156 | } 157 | } 158 | 159 | div.badge { 160 | padding-top: 3px; 161 | float: right; 162 | 163 | button { 164 | display: block; 165 | margin-bottom: 5px; 166 | border: none; 167 | outline: none; 168 | font-size: 12px; 169 | width: 215px; 170 | margin-top: 2px; 171 | padding: 2px; 172 | text-align: center; 173 | cursor: pointer; 174 | background-color: #777; 175 | color: #ffffff; 176 | &:hover { 177 | background-color: #229fe5; 178 | } 179 | } 180 | } 181 | 182 | dl { 183 | margin: 0; 184 | padding: 5px 20px; 185 | } 186 | 187 | dt { 188 | color: #5f5f5f; 189 | font-size: 12px; 190 | font-weight: normal; 191 | margin-bottom: 5px; 192 | } 193 | 194 | dd { 195 | margin: 0; 196 | padding: 0; 197 | color: #fff; 198 | line-height: 20px; 199 | font-size: 26px; 200 | font-weight: bold; 201 | letter-spacing: 0.075em; 202 | 203 | span { 204 | color: #6c6c6c; 205 | } 206 | } 207 | 208 | button.badge { 209 | float: right; 210 | } 211 | } 212 | 213 | /* Search */ 214 | section.search { 215 | width: 100%; 216 | height: 60px; 217 | min-width: 1032px; 218 | background: #fff; 219 | position: relative; 220 | z-index: 99; 221 | 222 | &.sticky { 223 | position: fixed; 224 | top: 0; 225 | left: 0; 226 | border-bottom: 1px solid #ccc; 227 | } 228 | 229 | .manage-images { 230 | float: left; 231 | 232 | h3 { 233 | float: left; 234 | margin-top: 20px; 235 | margin-left: 40px; 236 | } 237 | 238 | .plus { 239 | margin: 22px 0 0 15px; 240 | } 241 | 242 | input[reset-field] { 243 | float: none; 244 | margin: 10px 0 0 25px; 245 | width: 300px; 246 | padding-right: 19px; 247 | 248 | &::-ms-clear { 249 | display: none; 250 | } 251 | 252 | &::-webkit-search-cancel-button { 253 | -webkit-appearance: none; 254 | } 255 | 256 | ~ .reset-icon { 257 | display: block; 258 | position: absolute; 259 | right: 45px; 260 | top: 25px; 261 | width: 15px; 262 | height: 15px; 263 | @include icon-grey; 264 | @extend .icon-x; 265 | cursor: pointer; 266 | 267 | &:hover { 268 | @include icon-red; 269 | } 270 | 271 | &.hidden { 272 | display: none; 273 | } 274 | } 275 | } 276 | } 277 | .sharing { 278 | float: right; 279 | padding-right: 40px; 280 | span, div { 281 | display: inline-block; 282 | } 283 | > span { 284 | position: relative; 285 | top: -8px; 286 | margin-right: 8px; 287 | } 288 | .addthis-icons { 289 | padding-right: 15px; 290 | border-right: 1px solid #ccc; 291 | margin-right: 14px; 292 | margin-top: 14px; 293 | a[class*=at-svc] { 294 | float: left; 295 | position: relative; 296 | &:after { 297 | height: 25px; 298 | width: 25px; 299 | content: ''; 300 | display: block; 301 | top: 5px; 302 | left: 5px; 303 | position: absolute; 304 | cursor: pointer; 305 | } 306 | } 307 | 308 | $sharing-icons: (facebook, twitter, linkedin, google_plus); 309 | @each $icon in $sharing-icons { 310 | a[class*=#{$icon}]:after { 311 | @extend .icon-#{$icon}-xlarge; 312 | @include icon-light-grey; 313 | } 314 | a[class*=#{$icon}]:hover:after { 315 | @include icon-grey; 316 | } 317 | } 318 | 319 | .at-circular { 320 | background-color: transparent; 321 | } 322 | 323 | span[class*=aticon] { 324 | display: none; 325 | } 326 | } 327 | 328 | .icon-button.copy { 329 | float: none; 330 | margin-left: 0; 331 | margin-top: 10px; 332 | position: relative; 333 | top: -8px; 334 | } 335 | } 336 | } 337 | 338 | //general button style 339 | .button { 340 | color: white; 341 | font-size: 14px; 342 | padding: 15px; 343 | width: 310px; 344 | background-color: #777777; 345 | display: inline-block; 346 | margin: 10px 20px; 347 | cursor: pointer; 348 | text-align: center; 349 | &.action { 350 | background-color: #91B748; 351 | } 352 | } 353 | 354 | /* Footer */ 355 | footer { 356 | width: 100%; 357 | height: 120px; 358 | position: fixed; 359 | padding-bottom: 10px; 360 | bottom: 0; 361 | left: 0; 362 | z-index: 10; 363 | background: #2d2d2d; 364 | 365 | .draggable { 366 | width: 100%; 367 | height: 12px; 368 | padding-top: 3px; 369 | 370 | &.dragging { 371 | .drag-handle { 372 | background-position: 0 -9px; 373 | } 374 | } 375 | 376 | .drag-handle { 377 | cursor: move; 378 | width: 30px; 379 | height: 8px; 380 | background: url('../images/icon_drag.svg') 0 -2px no-repeat; 381 | background-size: 30px 20px; 382 | margin: 0 auto; 383 | 384 | &:hover { 385 | background-position: 0 -9px; 386 | } 387 | } 388 | } 389 | 390 | /* Journal */ 391 | section.journal { 392 | width: 100%; 393 | height: 100%; 394 | overflow: auto; 395 | font-family: monospace; 396 | padding: 10px 20px; 397 | font-size: 0.95em; 398 | line-height: 1.35em; 399 | color: #a0a0a0; 400 | 401 | li { 402 | margin: 5px 0; 403 | &:last-child { 404 | color: #fff; 405 | } 406 | } 407 | } 408 | } 409 | 410 | .mobile { 411 | position: absolute; 412 | top: 0; 413 | left: 0; 414 | height: 100%; 415 | width: 100%; 416 | z-index: 9999; 417 | background-color: #f1f1f1; 418 | text-align: center; 419 | .inner { 420 | width: 90%; 421 | min-width: 300px; 422 | margin: 10% auto 0; 423 | 424 | h1.logo { 425 | width: 100%; 426 | height: 100px; 427 | max-width: 600px; 428 | text-indent: -9999px; 429 | margin: 0 auto; 430 | color: transparent; 431 | background: url('../images/logo_image_layers.svg') no-repeat 10px center; 432 | } 433 | 434 | .button-wrap { 435 | text-align: center; 436 | .button { 437 | margin-left: 0%; 438 | margin-right: 0; 439 | width: 100%; 440 | text-decoration: none; 441 | } 442 | } 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /app/styles/mobile.scss: -------------------------------------------------------------------------------- 1 | body.mobile-device { 2 | footer, 3 | .badge, 4 | .search, 5 | nav.ctl, 6 | cite.cloud, 7 | .landing .hub { 8 | display: none; 9 | } 10 | 11 | main { 12 | bottom: 0; 13 | cite.logo { 14 | margin-right: 10px; 15 | } 16 | section.metrics { 17 | width: 100%; 18 | min-width: 300px; 19 | overflow: hidden; 20 | li { 21 | width: 50%; 22 | float: left; 23 | border-right: none; 24 | dl { 25 | padding: 5px; 26 | } 27 | } 28 | } 29 | 30 | nav { 31 | &.sticky { 32 | width: 100%; 33 | } 34 | 35 | &:not(.sticky) { 36 | div.leaves { 37 | &:before, 38 | &:after { 39 | display: none; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | .blog-link { 46 | position: fixed; 47 | bottom: 0; 48 | width: 100%; 49 | padding: 15px 0; 50 | text-align: center; 51 | background-color: #ffffff; 52 | color: #333; 53 | font-weight: bold; 54 | &:visited { 55 | color: #333; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/vendor/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/vendor/ZeroClipboard.swf -------------------------------------------------------------------------------- /app/views/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/app/views/.DS_Store -------------------------------------------------------------------------------- /app/views/about-menu.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 |
7 |

8 | Visualize Docker images and the layers that compose them. See how each command in the Dockerfile 9 | contributes to the final image, and discover which layers are shared by multiple images. 10 |

11 |

Important Note

12 |
13 |

14 | ImageLayers doesn't currently work with images using V2 of the Docker Registry API. We're working to fix this. In the meantime you can 15 | get a similar badge showing layers and download size from MicroBadger. 16 |

17 |

18 | ImageLayers UI on GitHub 19 | ImageLayers API on GitHub 20 |

21 |

22 | ImageLayers is released under the Apache License
23 | To report bugs, or suggest new features log a GitHub Issue 24 |

25 | 38 | 41 |
42 | -------------------------------------------------------------------------------- /app/views/badge.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /app/views/badgeDialog.html: -------------------------------------------------------------------------------- 1 |
2 | Get an Image Badge 3 |
4 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 | 33 |
34 |
    35 |
  • 36 | 37 | 38 |
    40 | Copy to Clipboard 41 | Copied to Clipboard 42 |
    43 |
  • 44 |
  • 45 | 46 | 47 |
    49 | Copy to Clipboard 50 | Copied to Clipboard 51 |
    52 |
  • 53 |
  • 54 | 55 | 56 |
    58 | Copy to Clipboard 59 | Copied to Clipboard 60 |
    61 |
  • 62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /app/views/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | ImageLayers 5 |

6 |
7 | 8 | Hosted in Kubernetes 9 | 10 | 13 |
14 | 17 |
18 |
19 |
20 | 21 | 22 |
23 | 40 |
41 |
42 |
43 | 51 | -------------------------------------------------------------------------------- /app/views/grid.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 |

Welcome to ImageLayers

13 |

14 | Visualize Docker images and the layers that compose them.
15 | See how each command in the Dockerfile 16 | contributes to the final image, and discover which layers are shared by multiple images. 17 |

18 | 19 |
Search for images on the Docker Hub
20 | or 21 |
22 |
Load a sample image set
23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /app/views/imageSearch.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 | 7 | -------------------------------------------------------------------------------- /app/views/leaf.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
Layers: {{ leaf.count }}
6 |
7 | -------------------------------------------------------------------------------- /app/views/metrics.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /app/views/mobile.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

ImageLayers

4 |

ImageLayers is optimized for desktop.

5 |
6 |
Understood, but I still want to see
7 | Learn more about the project 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /app/views/searchDialog.html: -------------------------------------------------------------------------------- 1 |
2 | Manage Images 3 |
4 | 8 |
9 |
10 |
11 |
12 |
13 |
Add Another Row
14 |
15 |
16 | 22 | -------------------------------------------------------------------------------- /bin/deploy_qa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ssh root@8.22.8.236 "docker rm -f imagelayers_ui; docker rmi -f centurylink/imagelayers-ui:qa; docker pull centurylink/imagelayers-ui:qa; docker run -d --name imagelayers_ui -p 9000:9000 centurylink/imagelayers-ui:qa" -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iLayers", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "jquery": "^2.1.3", 6 | "angular": ">=1.3.15", 7 | "angular-animate": "~1.3.0", 8 | "angular-cookies": "~1.3.0", 9 | "angular-resource": "~1.3.0", 10 | "angular-route": "~1.3.0", 11 | "angular-sanitize": "~1.3.0", 12 | "ngDialog": "~0.3.12", 13 | "angular-scroll-glue": "2.0.4", 14 | "angular-zeroclipboard": "0.4.1" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "~1.3.0" 18 | }, 19 | "resolutions": { 20 | "angular": ">=1.3.15" 21 | }, 22 | "appPath": "app", 23 | "moduleName": "iLayers" 24 | } 25 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.10.35 4 | services: 5 | - docker 6 | 7 | dependencies: 8 | pre: 9 | - npm install -g npm@2.6.0 10 | - npm install -g karma 11 | - npm install -g bower 12 | - npm install -g grunt-cli 13 | post: 14 | - gem install compass 15 | - bower install 16 | 17 | test: 18 | override: 19 | - grunt test 20 | 21 | deployment: 22 | hub: 23 | branch: master 24 | commands: 25 | - export IMAGE_LAYERS_API=$QA_API_IP && grunt build 26 | - docker build -t centurylink/imagelayers-ui:qa . 27 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 28 | - docker push centurylink/imagelayers-ui:qa 29 | Production: 30 | branch: release 31 | commands: 32 | - export IMAGE_LAYERS_API=$PRO_API_IP1 && grunt build 33 | - docker build -t centurylink/imagelayers-ui:latest . 34 | - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS 35 | - docker push centurylink/imagelayers-ui:latest 36 | 37 | -------------------------------------------------------------------------------- /deployment/README.md: -------------------------------------------------------------------------------- 1 | # Deploying imagelayers in to a Kubernetes cluster 2 | Imagelayers is a set of three services 3 | - imagelayers frontend, serving up the angular interface 4 | - imagelayers api, communicating with docker hub or other container registry 5 | - badger, constructing imagelayers svg badge graphics objects 6 | 7 | None of the three core services are exposed directly -- the imagelayers and 8 | badger services services are exposed externally using an Elastic Load Balancer 9 | (ELB) from AWS which terminates TLS. 10 | 11 | The imagelayers-web proxy is the main point of contact for the 12 | angular client interface. In a production system, that endpoint will be 13 | [https://imagelayers.io](https://imagelayers.io). This web service allows 14 | the client to communicate with the imagelayers-api by proxying the path 15 | _/registry_ and with the badge service by proxying the path _/badge_. 16 | 17 | The badge service is exposed to external clients through a second ELB, 18 | expected to have the endpoint 19 | [https://badge.imagelayers.io](https://badge.imagelayers.io). 20 | 21 | # Deploying imagelayers 22 | Each of these three imagelayers components has associated Kubernetes service 23 | and pods. They are deployed with the _kubectl_ binary and the manifests found 24 | in the _deployment/_ subdirectory. 25 | 26 | Edit the Services 27 | 28 | The imagelayers-web and badger services use SSL certificates from ACM 29 | (AWS Certificate Manager). Get the ARN for the certificates from the 30 | [AWS console](http://docs.aws.amazon.com/acm/latest/userguide/gs-acm-manage.html). 31 | 32 | ``` 33 | annotations: 34 | service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:us-east-1:*****:certificate/***** 35 | service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http 36 | service.beta.kubernetes.io/aws-load-balancer-ssl-ports: https 37 | ``` 38 | 39 | Create the Services 40 | ``` 41 | kubectl --namespace=staging -f deployment/imagelayers-svc.yml 42 | kubectl --namespace=staging -f deployment/badger-svc.yml 43 | kubectl --namespace=staging -f deployment/imagelayers-web-svc.yml 44 | ``` 45 | 46 | Create the Deployments (including ReplicaSets and Pods) 47 | ``` 48 | kubectl --namespace=staging -f deployment/imagelayers-deployment.yml 49 | kubectl --namespace=staging -f deployment/badger-deployment.yml 50 | kubectl --namespace=staging -f deployment/imagelayers-web-deployment.yml 51 | ``` 52 | 53 | # Upgrading imagelayers-web 54 | 55 | Using Kubernetes [Deployments](http://kubernetes.io/docs/user-guide/deployments/) 56 | will make most updates straightforward. 57 | 58 | `kubectl edit deployment/imagelayers-web` will allow you to edit the details of 59 | the pod specs and configurations, and apply a rolling update to the system. It 60 | is strongly advised, however, to read the relevant Kubernetes documentation as 61 | the recommended update processes may well have changed since this writing. 62 | 63 | # Alternate solution for SSL 64 | 65 | If you don't want to use ELB or ACM you can use an nginx proxy to terminate TLS. 66 | 67 | ## Deploying the TLS proxy 68 | We'll use an ansible playbook which creates the Kubernetes manifests for the 69 | ssl-proxy deployment and copies the TLS certificates into a Kubernetes secret. 70 | The details of this process are described in 71 | [https://github.com/ntfrnzn/kubernetes_sslproxy](https://github.com/ntfrnzn/kubernetes_sslproxy) 72 | 73 | This process sets up the proxy service as type LoadBalancer, so an external p 74 | ublic ip address will be created automatically. 75 | 76 | Have your certificates in a local directory, this deployment expects to find the 77 | following files: 78 | - imagelayers-web.crt _(with intermediate certificates concatenated)_ 79 | - imagelayers-web.key 80 | - badger.crt _(with intermediate certificates concatenated)_ 81 | - badger.key 82 | 83 | ``` 84 | K8SP=/tmp/kubernetes_sslproxy 85 | 86 | git clone https://github.com/ntfrnzn/kubernetes_sslproxy ${K8SP} 87 | 88 | CERT_DIR=$(pwd)/ssl/ 89 | 90 | export KUBECONFIG=/path/to/kube/config 91 | 92 | ansible-playbook \ 93 | -e service_name=imagelayers-web \ 94 | -e cert_dir=${CERT_DIR} \ 95 | -e service_namespace=staging \ 96 | ${K8SP}/install_sslproxy.yml 97 | 98 | ansible-playbook \ 99 | -e service_name=badger \ 100 | -e cert_dir=${CERT_DIR} \ 101 | -e service_namespace=staging \ 102 | ${K8SP}/install_sslproxy.yml 103 | ``` 104 | 105 | This process copied (base64-encoded) the certificates into a Kubernetes secrets 106 | manifest, so it's best to clean that up 107 | 108 | ``` 109 | rm ${K8SP}/manifests/* 110 | ``` 111 | -------------------------------------------------------------------------------- /deployment/badger-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: badger 5 | spec: 6 | replicas: 2 7 | strategy: 8 | rollingUpdate: 9 | maxSurge: 1 10 | maxUnavailable: 1 11 | type: RollingUpdate 12 | template: 13 | metadata: 14 | labels: 15 | app: badger 16 | name: badger 17 | spec: 18 | containers: 19 | - env: 20 | - name: MODE 21 | value: production 22 | image: microscaling/imagelayers-badger:latest 23 | imagePullPolicy: IfNotPresent 24 | name: badger 25 | resources: 26 | limits: 27 | cpu: 75m 28 | memory: 30Mi 29 | requests: 30 | cpu: 50m 31 | memory: 20Mi 32 | securityContext: 33 | privileged: false 34 | terminationMessagePath: /dev/termination-log 35 | dnsPolicy: ClusterFirst 36 | restartPolicy: Always 37 | securityContext: {} 38 | terminationGracePeriodSeconds: 30 39 | -------------------------------------------------------------------------------- /deployment/badger-svc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: badger 6 | name: badger 7 | spec: 8 | ports: 9 | - port: 80 10 | protocol: TCP 11 | targetPort: 3000 12 | name: http 13 | selector: 14 | app: badger 15 | sessionAffinity: None 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /deployment/imagelayers-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: imagelayers 5 | spec: 6 | replicas: 2 7 | strategy: 8 | rollingUpdate: 9 | maxSurge: 1 10 | maxUnavailable: 1 11 | type: RollingUpdate 12 | template: 13 | metadata: 14 | labels: 15 | app: imagelayers 16 | name: imagelayers 17 | spec: 18 | containers: 19 | - name: imagelayers 20 | image: microscaling/imagelayers-api:1.1.2 21 | imagePullPolicy: IfNotPresent 22 | resources: 23 | limits: 24 | cpu: 75m 25 | memory: 30Mi 26 | requests: 27 | cpu: 50m 28 | memory: 20Mi 29 | securityContext: 30 | privileged: false 31 | terminationMessagePath: /dev/termination-log 32 | dnsPolicy: ClusterFirst 33 | restartPolicy: Always 34 | securityContext: {} 35 | terminationGracePeriodSeconds: 30 36 | -------------------------------------------------------------------------------- /deployment/imagelayers-production-ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: imagelayers 5 | annotations: 6 | ingress.kubernetes.io/ssl-redirect: "true" 7 | kubernetes.io/tls-acme: "true" 8 | certmanager.k8s.io/issuer: letsencrypt-production 9 | kubernetes.io/ingress.class: "nginx" 10 | spec: 11 | tls: 12 | - hosts: 13 | - imagelayers.io 14 | - badge.imagelayers.io 15 | secretName: imagelayers-letsencrypt 16 | rules: 17 | - host: imagelayers.io 18 | http: 19 | paths: 20 | - path: / 21 | backend: 22 | serviceName: imagelayers-web 23 | servicePort: 80 24 | - host: badge.imagelayers.io 25 | http: 26 | paths: 27 | - path: / 28 | backend: 29 | serviceName: badger 30 | servicePort: 80 31 | -------------------------------------------------------------------------------- /deployment/imagelayers-staging-ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: imagelayers 5 | annotations: 6 | ingress.kubernetes.io/ssl-redirect: "true" 7 | kubernetes.io/tls-acme: "true" 8 | certmanager.k8s.io/issuer: letsencrypt-production 9 | kubernetes.io/ingress.class: "nginx" 10 | spec: 11 | tls: 12 | - hosts: 13 | - staging.imagelayers.io 14 | secretName: imagelayers-letsencrypt 15 | rules: 16 | - host: staging.imagelayers.io 17 | http: 18 | paths: 19 | - path: / 20 | backend: 21 | serviceName: imagelayers-web 22 | servicePort: 80 23 | -------------------------------------------------------------------------------- /deployment/imagelayers-svc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: imagelayers 6 | name: imagelayers 7 | spec: 8 | ports: 9 | - port: 80 10 | protocol: TCP 11 | targetPort: 8888 12 | selector: 13 | app: imagelayers 14 | sessionAffinity: None 15 | type: ClusterIP 16 | -------------------------------------------------------------------------------- /deployment/imagelayers-web-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: imagelayers-web 5 | spec: 6 | replicas: 2 7 | strategy: 8 | rollingUpdate: 9 | maxSurge: 1 10 | maxUnavailable: 1 11 | type: RollingUpdate 12 | template: 13 | metadata: 14 | labels: 15 | app: imagelayers-web 16 | name: imagelayers-web 17 | spec: 18 | containers: 19 | - name: imagelayers-web 20 | image: microscaling/imagelayers-web:1.1.2 21 | imagePullPolicy: IfNotPresent 22 | name: imagelayers-web 23 | resources: 24 | limits: 25 | cpu: 75m 26 | memory: 20Mi 27 | requests: 28 | cpu: 50m 29 | memory: 10Mi 30 | securityContext: 31 | privileged: false 32 | terminationMessagePath: /dev/termination-log 33 | dnsPolicy: ClusterFirst 34 | restartPolicy: Always 35 | securityContext: {} 36 | terminationGracePeriodSeconds: 30 37 | -------------------------------------------------------------------------------- /deployment/imagelayers-web-svc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app: imagelayers-web 6 | name: imagelayers-web 7 | spec: 8 | ports: 9 | - port: 80 10 | protocol: TCP 11 | targetPort: 9000 12 | name: http 13 | selector: 14 | app: imagelayers-web 15 | sessionAffinity: None 16 | type: ClusterIP 17 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nobody nogroup; 2 | worker_processes 1; 3 | 4 | error_log /dev/stderr warn; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | include mime.types; 12 | 13 | log_format main '{' 14 | '"body_bytes_sent":"$body_bytes_sent",' 15 | '"host":"$host",' 16 | '"hostname":"$hostname",' 17 | '"http_forwarded":"$http_forwarded",' 18 | '"http_referer":"$http_referer",' 19 | '"http_user_agent":"$http_user_agent",' 20 | '"http_x_forwarded_for":"$http_x_forwarded_for",' 21 | '"query_string":"$query_string",' 22 | '"remote_addr":"$remote_addr",' 23 | '"remote_user":"$remote_user",' 24 | '"request_method":"$request_method",' 25 | '"request_time":"$request_time",' 26 | '"request":"$request",' 27 | '"scheme":"$scheme",' 28 | '"server_port":"$server_port",' 29 | '"status":"$status",' 30 | '"time_iso8601":"$time_iso8601",' 31 | '"upstream_addr":"$upstream_addr",' 32 | '"upstream_response_time":"$upstream_response_time",' 33 | '"upstream_status":"$upstream_status"' 34 | '}'; 35 | 36 | access_log /dev/stdout main; 37 | # error_log /dev/stdout debug; 38 | 39 | sendfile on; 40 | tcp_nopush on; 41 | 42 | keepalive_timeout 65; 43 | 44 | gzip on; 45 | 46 | upstream imagelayers-api { 47 | server imagelayers; 48 | } 49 | 50 | upstream badger { 51 | server badger; 52 | } 53 | 54 | ## health and status check 55 | server { 56 | listen 8080; 57 | 58 | location /nginx_status { 59 | stub_status on; 60 | access_log off; 61 | } 62 | } 63 | 64 | # used only by clients directly addressing this service 65 | server { 66 | listen 9000; 67 | server_name badge.imagelayers.io; 68 | 69 | location / { 70 | access_log /dev/stdout main; 71 | 72 | proxy_redirect off; 73 | 74 | proxy_set_header Host 'badge.imagelayers.io'; 75 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 76 | proxy_set_header Connection 'Keep-Alive'; 77 | 78 | proxy_pass http://badger/; 79 | } 80 | 81 | error_page 404 500 502 503 504 /error.html; 82 | location = /error.html { 83 | root html; 84 | } 85 | } 86 | 87 | server { 88 | listen 9000 default_server; 89 | server_name imagelayers.io; 90 | 91 | location / { 92 | root /data/dist; 93 | index index.html index.htm; 94 | expires 1h; 95 | add_header Cache-Control "public"; 96 | try_files $uri $uri/ /index.html; 97 | } 98 | 99 | # forward all requests for /registry to the internal imagelayers-api service 100 | location /registry { 101 | access_log /dev/stdout main; 102 | 103 | rewrite /registry/(.*) /registry/$1 break; 104 | proxy_redirect off; 105 | 106 | proxy_set_header Host 'imagelayers.io'; 107 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 108 | proxy_set_header Connection 'Keep-Alive'; 109 | 110 | proxy_pass http://imagelayers-api/; 111 | } 112 | 113 | # forward all requests for /badge to the internal badge service 114 | location /badge { 115 | access_log /dev/stdout main; 116 | 117 | rewrite /badge/(.*) /$1 break; 118 | proxy_redirect off; 119 | 120 | proxy_set_header Host 'badge.imagelayers.io'; 121 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 122 | proxy_set_header Connection 'Keep-Alive'; 123 | 124 | proxy_pass http://badger/; 125 | } 126 | 127 | error_page 404 500 502 503 504 /error.html; 128 | location = /error.html { 129 | root html; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imagelayers", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "repository": {}, 6 | "devDependencies": { 7 | "grunt": "^0.4.5", 8 | "grunt-autoprefixer": "^2.0.0", 9 | "grunt-concurrent": "^1.0.0", 10 | "grunt-contrib-clean": "^0.6.0", 11 | "grunt-contrib-compass": "^1.0.0", 12 | "grunt-contrib-concat": "^0.5.0", 13 | "grunt-contrib-connect": "^0.9.0", 14 | "grunt-contrib-copy": "^0.7.0", 15 | "grunt-contrib-cssmin": "^0.12.0", 16 | "grunt-contrib-htmlmin": "^0.4.0", 17 | "grunt-contrib-imagemin": "^0.9.2", 18 | "grunt-contrib-jshint": "^0.11.0", 19 | "grunt-contrib-uglify": "^0.7.0", 20 | "grunt-contrib-watch": "^0.6.1", 21 | "grunt-filerev": "^2.1.2", 22 | "grunt-google-cdn": "^0.4.3", 23 | "grunt-karma": "^0.10.1", 24 | "grunt-newer": "^1.1.0", 25 | "grunt-ng-annotate": "^0.9.2", 26 | "grunt-ng-constant": "^1.1.0", 27 | "grunt-svgmin": "^2.0.0", 28 | "grunt-usemin": "^3.0.0", 29 | "grunt-wiredep": "^2.0.0", 30 | "jasmine-core": "^2.2.0", 31 | "jshint-stylish": "^1.0.0", 32 | "karma": "^0.12.31", 33 | "karma-jasmine": "^0.3.5", 34 | "karma-ng-html2js-preprocessor": "^0.1.2", 35 | "karma-phantomjs-launcher": "^0.1.4", 36 | "karma-spec-reporter": "0.0.12", 37 | "load-grunt-tasks": "^3.1.0", 38 | "time-grunt": "^1.0.0" 39 | }, 40 | "engines": { 41 | "node": ">=0.10.0" 42 | }, 43 | "scripts": { 44 | "test": "grunt test" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/test/.DS_Store -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "jasmine": true, 22 | "globals": { 23 | "angular": false, 24 | "browser": false, 25 | "inject": false 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2015-03-09 using 4 | // generator-karma 0.9.0 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | frameworks: ['jasmine'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | // bower:js 22 | 'bower_components/jquery/dist/jquery.js', 23 | 'bower_components/angular/angular.js', 24 | 'bower_components/angular-animate/angular-animate.js', 25 | 'bower_components/angular-cookies/angular-cookies.js', 26 | 'bower_components/angular-resource/angular-resource.js', 27 | 'bower_components/angular-route/angular-route.js', 28 | 'bower_components/angular-sanitize/angular-sanitize.js', 29 | 'bower_components/ngDialog/js/ngDialog.js', 30 | 'bower_components/angular-scroll-glue/src/scrollglue.js', 31 | 'bower_components/zeroclipboard/dist/ZeroClipboard.js', 32 | 'bower_components/angular-zeroclipboard/src/angular-zeroclipboard.js', 33 | 'bower_components/angular-mocks/angular-mocks.js', 34 | // endbower 35 | 'app/lib/**/*.js', 36 | 'app/scripts/config.js', 37 | 'app/scripts/**/*.js', 38 | 'test/mock/**/*.js', 39 | 'test/spec/**/*.js', 40 | '**/*.html' 41 | ], 42 | 43 | // list of files / patterns to exclude 44 | exclude: [ 45 | ], 46 | 47 | // web server port 48 | port: 8080, 49 | 50 | // Start these browsers, currently available: 51 | // - Chrome 52 | // - ChromeCanary 53 | // - Firefox 54 | // - Opera 55 | // - Safari (only Mac) 56 | // - PhantomJS 57 | // - IE (only Windows) 58 | browsers: [ 59 | 'PhantomJS' 60 | ], 61 | 62 | preprocessors: { 63 | '**/*.html': ['ng-html2js'] 64 | }, 65 | 66 | ngHtml2JsPreprocessor: { 67 | stripPrefix: 'app/' 68 | }, 69 | 70 | // Which plugins to enable 71 | plugins: [ 72 | 'karma-phantomjs-launcher', 73 | 'karma-jasmine', 74 | 'karma-spec-reporter', 75 | 'karma-ng-html2js-preprocessor' 76 | ], 77 | 78 | reporters: ['spec'], 79 | 80 | // Continuous Integration mode 81 | // if true, it capture browsers, run tests and exit 82 | singleRun: false, 83 | 84 | colors: true, 85 | 86 | // level of logging 87 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 88 | logLevel: config.LOG_INFO, 89 | 90 | // Uncomment the following lines if you are using grunt's server to run the tests 91 | // proxies: { 92 | // '/': 'http://localhost:9000/' 93 | // }, 94 | // URL root prevent conflicts with the site root 95 | // urlRoot: '_karma_' 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /test/spec/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microscaling/imagelayers-graph/8aff908c7c86af18c7f6f608da25cbb2936cb148/test/spec/.DS_Store -------------------------------------------------------------------------------- /test/spec/controllers/badgedialog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: BadgedialogCtrl', function() { 4 | 5 | // load the controller's module 6 | beforeEach(module('iLayers')); 7 | 8 | var controller, 9 | scope, 10 | graph, 11 | deferredSuccess, 12 | registryService; 13 | 14 | 15 | // Initialize the controller and a mock scope 16 | beforeEach(inject(function($controller, $rootScope, _registryService_) { 17 | scope = $rootScope.$new(); 18 | registryService = _registryService_; 19 | controller = $controller('BadgeDialogCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | describe('$watch selectedWorkflow', function() { 25 | it('should initialze selectedImage to empty image', function() { 26 | scope.selectedImage = { 27 | name: 'boo' 28 | }; 29 | scope.$digest(); 30 | 31 | expect(scope.selectedImage.name).toEqual(''); 32 | expect(scope.selectedImage.tag).toEqual('latest'); 33 | }); 34 | }); 35 | 36 | describe('$watch selectedImage', function() { 37 | it('should set htmlCopied to false', function() { 38 | scope.htmlCopied = true; 39 | scope.selectedImage = { 40 | name: 'foo' 41 | }; 42 | scope.$digest(); 43 | 44 | expect(scope.htmlCopied).toBeFalsy(); 45 | }); 46 | 47 | it('should set markdownCopied to false', function() { 48 | scope.markdownCopied = true; 49 | scope.selectedImage = { 50 | name: 'foo' 51 | }; 52 | 53 | expect(scope.markdownCopied).toBeTruthy(); 54 | scope.$digest(); 55 | 56 | expect(scope.markdownCopied).toBeFalsy(); 57 | }); 58 | 59 | it('should set asciiDocCopied to false', function() { 60 | scope.asciiDocCopied = true; 61 | scope.selectedImage = { 62 | name: 'foo' 63 | }; 64 | 65 | expect(scope.asciiDocCopied).toBeTruthy(); 66 | scope.$digest(); 67 | 68 | expect(scope.asciiDocCopied).toBeFalsy(); 69 | }); 70 | }); 71 | 72 | describe('when workflow is imagelayers', function() { 73 | it('set selectedImage.selected = true', function() { 74 | scope.selectedWorkflow = 'imagelayers'; 75 | scope.$digest(); 76 | scope.selectedImage = { 77 | name: 'foo' 78 | }; 79 | 80 | expect(scope.selectedImage.selected).toBeFalsy(); 81 | scope.$digest(); 82 | 83 | expect(scope.selectedImage.selected).toBeTruthy(); 84 | }) 85 | }); 86 | 87 | describe('when workflow is hub', function() { 88 | it('set selectedImage.selected = false when missing', function() { 89 | scope.selectedWorkflow = 'hub'; 90 | scope.$digest(); 91 | 92 | scope.selectedImage = { 93 | name: 'foo', 94 | missing: true, 95 | selected: true 96 | }; 97 | 98 | expect(scope.selectedImage.selected).toBeTruthy(); 99 | scope.$digest(); 100 | 101 | expect(scope.selectedImage.selected).toBeFalsy(); 102 | }) 103 | }); 104 | 105 | 106 | describe('$scope.imageList', function() { 107 | beforeEach(function() { 108 | scope.graph = [{ 109 | repo: { 110 | name: 'myRepo', 111 | tag: 'latest' 112 | } 113 | }, { 114 | repo: { 115 | name: 'myOtherRepo', 116 | tag: 'latest' 117 | } 118 | }] 119 | }); 120 | 121 | it('should return a list of images', function() { 122 | var list = scope.imageList(); 123 | expect(list.length).toEqual(2); 124 | expect(list[0].name).toEqual('myRepo'); 125 | expect(list[1].name).toEqual('myOtherRepo'); 126 | }); 127 | }); 128 | 129 | describe('$scope.badgeAsHtml', function() { 130 | it('should return an HTML embed code', function() { 131 | scope.selectedImage = { 132 | name: 'node', 133 | tag: 'latest', 134 | selected: true 135 | }; 136 | var embedCode = scope.badgeAsHtml(); 137 | expect(embedCode.$$unwrapTrustedValue()).toEqual( 138 | "" 139 | ); 140 | }); 141 | }); 142 | 143 | 144 | describe('$scope.badgeAsMarkdown', function() { 145 | it('should return a Markdown embed code', function() { 146 | scope.selectedImage = { 147 | name: 'node', 148 | tag: 'latest', 149 | selected: true 150 | }; 151 | var embedCode = scope.badgeAsMarkdown(); 152 | expect(embedCode).toEqual( 153 | "[![](https://imagelayers.io/badge/node:latest.svg)](https://imagelayers.io/?images=node:latest 'Get your own badge on imagelayers.io')" 154 | ); 155 | }); 156 | }); 157 | 158 | describe('$scope.badgeAsAsciiDoc', function() { 159 | it('should return AsciiDoc embed code', function() { 160 | scope.selectedImage = { 161 | name: 'node', 162 | tag: 'latest', 163 | selected: true 164 | }; 165 | var embedCode = scope.badgeAsAsciiDoc(); 166 | expect(embedCode).toEqual( 167 | 'image:https://badge.imagelayers.io/node:latest.svg[title="Get your own badge on imagelayers.io", alt="Get your own badge on imagelayers.io", link="https://imagelayers.io/?images=node:latest"]' 168 | ); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/spec/controllers/dashboard.js: -------------------------------------------------------------------------------- 1 | describe('DashboardCtrl', function() { 2 | // Load the module 3 | beforeEach(module('iLayers')); 4 | 5 | var ctrl, scope, layers, registryService, commandService, data; 6 | 7 | beforeEach(inject(function ($controller, $rootScope, $window, _registryService_, _commandService_) { 8 | scope = $rootScope.$new(); 9 | 10 | registryService = _registryService_; 11 | commandService = _commandService_; 12 | 13 | ctrl = $controller('DashboardCtrl', { 14 | $scope: scope 15 | }); 16 | 17 | var window = $window; 18 | })); 19 | 20 | it('should initialize graph', function() { 21 | expect(scope.graph.length).toEqual(0); 22 | }); 23 | 24 | describe('buildTerms', function () { 25 | it('should add latest tag when empty', function() { 26 | var data = ctrl.buildTerms("foo"); 27 | 28 | expect(data[0].tag).toEqual("latest"); 29 | expect(data[0].name).toEqual("foo"); 30 | }); 31 | 32 | it('should return tag and name when provided', function() { 33 | var data = ctrl.buildTerms("foo:1.0.0"); 34 | 35 | expect(data[0].tag).toEqual("1.0.0"); 36 | expect(data[0].name).toEqual("foo"); 37 | }); 38 | 39 | it('should create terms for each image provided', function() { 40 | var data = ctrl.buildTerms("foo:1.0.0, baz:2.0.0"); 41 | 42 | expect(data.length).toEqual(2); 43 | expect(angular 44 | .equals(data[0], { "name": "foo", "tag": "1.0.0" })) 45 | .toBeTruthy(); 46 | expect(angular 47 | .equals(data[1], { "name": "baz", "tag": "2.0.0" })) 48 | .toBeTruthy(); 49 | }); 50 | }); 51 | 52 | describe('detectMobile', function() { 53 | 54 | var setUserAgent = function(window, userAgent) { 55 | if (window.navigator.userAgent != userAgent) { 56 | var userAgentProp = { get: function () { return userAgent; } }; 57 | window.navigator = Object.create(navigator, { 58 | userAgent: userAgentProp 59 | }); 60 | } 61 | }; 62 | 63 | afterEach(inject(function ($window){ 64 | setUserAgent(window, $window.navigator.userAgent); 65 | })); 66 | 67 | it('should set $scope.mobile to false if the user agent string does not have mobile keywords', function (){ 68 | setUserAgent(window, 'desktop'); 69 | ctrl.detectMobile(); 70 | expect(scope.mobile).toEqual(false); 71 | }); 72 | 73 | it('should set $scope.mobile to true if the user agent is has the iPhone keyword', function (){ 74 | setUserAgent(window, 'iPhone'); 75 | ctrl.detectMobile(); 76 | expect(scope.mobile).toEqual(true); 77 | }); 78 | 79 | it('should set $scope.mobile to true if the user agent is has the iPod keyword', function (){ 80 | setUserAgent(window, 'iPod'); 81 | ctrl.detectMobile(); 82 | expect(scope.mobile).toEqual(true); 83 | }); 84 | 85 | it('should set $scope.mobile to true if the user agent is has the iPad keyword', function (){ 86 | setUserAgent(window, 'iPad'); 87 | ctrl.detectMobile(); 88 | expect(scope.mobile).toEqual(true); 89 | }); 90 | 91 | it('should set $scope.mobile to true if the user agent is has the android keyword', function (){ 92 | setUserAgent(window, 'android'); 93 | ctrl.detectMobile(); 94 | expect(scope.mobile).toEqual(true); 95 | }); 96 | 97 | it('should set $scope.mobile to true if the user agent is has the webOS keyword', function (){ 98 | setUserAgent(window, 'webOS'); 99 | ctrl.detectMobile(); 100 | expect(scope.mobile).toEqual(true); 101 | }); 102 | 103 | it('should set $scope.mobile to true if the user agent is has the iemobile keyword', function (){ 104 | setUserAgent(window, 'iemobile'); 105 | ctrl.detectMobile(); 106 | expect(scope.mobile).toEqual(true); 107 | }); 108 | }); 109 | 110 | describe('searchImages', function() { 111 | 112 | beforeEach(inject(function($q) { 113 | deferredSuccess = $q.defer(); 114 | spyOn(registryService, 'inspect').and.returnValue(deferredSuccess.promise); 115 | })); 116 | 117 | it('should do nothing when no images provided', function() { 118 | ctrl.searchImages({}); 119 | expect(registryService.inspect).not.toHaveBeenCalled(); 120 | deferredSuccess.resolve({data: {'repo': {}, 'layers': []}}); 121 | }); 122 | 123 | it('should call registryService with provided images', function() { 124 | ctrl.searchImages({'images': 'foo,bar:1.0.0'}); 125 | deferredSuccess.resolve({data: {'repo': {}, 'layers': []}}); 126 | expect(registryService.inspect).toHaveBeenCalledWith([{"name":"foo","tag":"latest"}, {"name":"bar","tag":"1.0.0"}]); 127 | }); 128 | 129 | it('should set loading to true while calling registryService', function() { 130 | ctrl.searchImages({'images': 'foo,bar:1.0.0'}); 131 | expect(scope.loading).toEqual(true); 132 | }); 133 | 134 | it('should set loading to false after registry inspect success', function() { 135 | ctrl.searchImages({'images': 'foo,bar:1.0.0'}); 136 | expect(scope.loading).toEqual(true); 137 | 138 | deferredSuccess.resolve({data: {'repo': {}, 'layers': []}}); 139 | expect(registryService.inspect).toHaveBeenCalledWith([{"name":"foo","tag":"latest"}, {"name":"bar","tag":"1.0.0"}]); 140 | 141 | deferredSuccess.promise.then(function(){ 142 | expect(scope.loading).toEqual(false); 143 | }); 144 | }); 145 | 146 | it('should not change the value of loading on error', function() { 147 | ctrl.searchImages({'images': 'foo,bar:1.0.0'}); 148 | expect(scope.loading).toEqual(true); 149 | 150 | deferredSuccess.reject({data: {'repo': {}, 'layers': []}}); 151 | expect(registryService.inspect).toHaveBeenCalledWith([{"name":"foo","tag":"latest"}, {"name":"bar","tag":"1.0.0"}]); 152 | 153 | deferredSuccess.promise.then(function(){},function(){ 154 | expect(scope.loading).toEqual(true); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('applyFilters', function() { 160 | 161 | beforeEach(function(){ 162 | data = [{ 'repo': { 'name': 'foo' } }, { 'repo': { 'name': 'bar' } }]; 163 | }); 164 | 165 | it('should remove repos with name matching filter', function() { 166 | var result = scope.applyFilters(data, 'foo'); 167 | 168 | expect(result.length).toEqual(1); 169 | expect(result[0].repo.name).toEqual('foo'); 170 | }); 171 | 172 | it('should call commandService.release()', function() { 173 | spyOn(commandService,'release'); 174 | scope.applyFilters(data, 'foo'); 175 | scope.$digest(); 176 | expect(commandService.release).toHaveBeenCalled(); 177 | }); 178 | 179 | it('should call commandService.lock()', function() { 180 | spyOn(commandService,'lock'); 181 | scope.applyFilters(data, 'foo'); 182 | scope.$digest(); 183 | expect(commandService.lock).toHaveBeenCalledWith(undefined); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /test/spec/controllers/journal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: JournalCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('iLayers')); 7 | 8 | var JournalCtrl, scope, data; 9 | 10 | // Initialize the controller and a mock scope 11 | beforeEach(inject(function ($controller, $rootScope) { 12 | scope = $rootScope.$new(); 13 | JournalCtrl = $controller('JournalCtrl', { 14 | $scope: scope 15 | }); 16 | })); 17 | 18 | describe('when image is not locked', function() { 19 | beforeEach(function() { 20 | scope.lockedImage = undefined; 21 | data = ['one'] 22 | }); 23 | 24 | it('should set $scope.commands', function() { 25 | scope.$broadcast('command-change', { commands: data }); 26 | expect(scope.commands.length).toEqual(1); 27 | expect(scope.commands[0]).toEqual('one'); 28 | }) 29 | 30 | }); 31 | 32 | describe('when image is locked', function() { 33 | beforeEach(function() { 34 | scope.lockedImage = 'locked'; 35 | scope.commands = ['foo', 'bar']; 36 | }); 37 | 38 | it('should not change $scope.commands', function() { 39 | scope.$broadcast('command-change', { commands: data }); 40 | expect(scope.commands.length).toEqual(2); 41 | expect(scope.commands[0]).toEqual('foo'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/spec/controllers/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: SearchCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('iLayers')); 7 | 8 | var SearchCtrl, 9 | location, 10 | dialog, 11 | ga, 12 | scope; 13 | 14 | // Initialize the controller and a mock scope 15 | beforeEach(inject(function ($controller, $rootScope, $location, _ngDialog_) { 16 | scope = $rootScope.$new(); 17 | location = $location; 18 | dialog = _ngDialog_; 19 | SearchCtrl = $controller('SearchCtrl', { 20 | $scope: scope 21 | }); 22 | })); 23 | 24 | it('should initialize searchList', function() { 25 | expect(scope.searchList.length).toEqual(1); 26 | }); 27 | 28 | describe('buildQueryParams', function() { 29 | describe('when a tag is not provided', function() { 30 | it('creates a name element', function() { 31 | var list = SearchCtrl.buildQueryParams([{ 'name':'foo', 'tag':'' }]); 32 | expect(list).toEqual('foo'); 33 | }); 34 | 35 | it('creates a comma list of names', function() { 36 | var list = SearchCtrl.buildQueryParams([{ 'name':'foo', 'tag':'' }, { 'name':'bar', 'tag':'' }]); 37 | expect(list).toEqual('foo,bar'); 38 | }); 39 | }); 40 | 41 | describe('when a tag is provided', function() { 42 | it('creates a name:tag pair', function() { 43 | var list = SearchCtrl.buildQueryParams([{ 'name':'foo', 'tag':'latest' }]); 44 | expect(list).toEqual('foo:latest'); 45 | }); 46 | 47 | it('creates a comma list of names', function() { 48 | var list = SearchCtrl.buildQueryParams([{ 'name':'foo', 'tag':'latest' }, { 'name':'bar', 'tag':'1' }]); 49 | expect(list).toEqual('foo:latest,bar:1'); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('populateSearch', function() { 55 | beforeEach(function() { 56 | location.search('images', 'foo:latest'); 57 | }); 58 | it('should create list from url search string', function() { 59 | var result = SearchCtrl.populateSearch(); 60 | expect(result[0].name).toEqual('foo'); 61 | expect(result[0].tag).toEqual('latest'); 62 | }); 63 | }); 64 | 65 | describe('$scope.showSearch', function() { 66 | it('should open dialog', function() { 67 | var options = { 68 | closeByDocument: false, 69 | template: 'views/searchDialog.html', 70 | className: 'ngdialog-theme-layers', 71 | controller: 'SearchCtrl' 72 | }; 73 | spyOn(dialog, 'open'); 74 | scope.showSearch(); 75 | expect(dialog.open).toHaveBeenCalledWith(options); 76 | }); 77 | }); 78 | 79 | describe('$scope.removeAll', function() { 80 | it('should set searchList to single empty image', function() { 81 | scope.searchList = [{ name: 'one', tag: 'oneTag' }, 82 | { name: 'two', tag: 'twoTag' }]; 83 | 84 | expect(scope.searchList.length).toEqual(2); 85 | 86 | scope.removeAll(); 87 | 88 | expect(scope.searchList.length).toEqual(1); 89 | expect(scope.searchList[0].name).toEqual(''); 90 | expect(scope.searchList[0].tag).toEqual('latest'); 91 | }); 92 | }); 93 | 94 | describe('$scope.addRow', function() { 95 | it('should add blank row to searchList', function() { 96 | scope.searchList = []; 97 | scope.addRow(); 98 | expect(scope.searchList.length).toEqual(1); 99 | expect(scope.searchList[0]).toEqual({ 'name': '', 'tag': 'latest' }); 100 | }); 101 | }); 102 | 103 | describe('$scope.closeDialog', function() { 104 | it('should call closeAll() on dialog', function() { 105 | spyOn(dialog, 'closeAll'); 106 | scope.closeDialog(); 107 | expect(dialog.closeAll).toHaveBeenCalled(); 108 | }); 109 | }); 110 | 111 | describe('$scope.showExampleSearch', function () { 112 | it('should add example images to $scope.selectedImages', function () { 113 | scope.showExampleSearch(); 114 | expect(scope.searchList.length).toEqual(6); 115 | }); 116 | it('should call addImages() for the searchList', function () { 117 | spyOn(scope, 'addImages'); 118 | scope.showExampleSearch(); 119 | expect(scope.addImages).toHaveBeenCalled(); 120 | }); 121 | }); 122 | 123 | describe('$scope.addImages', function() { 124 | beforeEach(function() { 125 | spyOn(location, 'search'); 126 | spyOn(scope, 'closeDialog'); 127 | scope.searchList = [{'name': 'foo', 'tag': 'latest', 'found': true }]; 128 | }); 129 | 130 | it('should remove empty image rows from list', function() { 131 | spyOn(location, 'url'); 132 | scope.searchList = [{'name': '', 'tag': ''}]; 133 | scope.addImages(); 134 | expect(location.url).toHaveBeenCalled(); 135 | }); 136 | 137 | it('should remove any image not found', function() { 138 | spyOn(location, 'url'); 139 | scope.searchList = [{'name': 'foo', 'tag': 'latest', 'missing': true}]; 140 | scope.addImages(); 141 | expect(location.url).toHaveBeenCalled(); 142 | }); 143 | 144 | it('should change the search string', function() { 145 | scope.addImages(); 146 | expect(location.search).toHaveBeenCalledWith('images', 'foo:latest'); 147 | }); 148 | 149 | it('should close the dialog', function() { 150 | scope.addImages(); 151 | expect(scope.closeDialog).toHaveBeenCalled(); 152 | }); 153 | }); 154 | 155 | describe('$scope.removeImage', function() { 156 | it('should remove the image by index', function() { 157 | scope.searchList = ['foo', 'bar', 'baz']; 158 | scope.removeImage(1); 159 | expect(scope.searchList).toEqual(['foo','baz']); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/spec/directives/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: about', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | // Load the templates 8 | beforeEach(module('views/about-menu.html')); 9 | 10 | var directive, scope, controller, elem, menu; 11 | 12 | beforeEach(inject(function ($compile, $rootScope) { 13 | var rootScope = $rootScope.$new(); 14 | elem = angular.element("
"); 15 | 16 | 17 | directive = $compile(elem)(rootScope); 18 | rootScope.$digest(); 19 | controller = elem.controller('about'); 20 | scope = elem.isolateScope(); 21 | menu = $('ul.about'); 22 | })); 23 | 24 | describe('scope.toggleMenu', function() { 25 | describe('when menu is visible and not active', function() { 26 | beforeEach(function() { 27 | scope.menuVisible = true; 28 | }); 29 | 30 | it('should call slideToggle', function() { 31 | spyOn(menu, 'slideToggle'); 32 | scope.toggleMenu(menu); 33 | 34 | expect(menu.slideToggle).toHaveBeenCalled(); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec/directives/draggable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: draggable', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | 8 | var directive, isoScope, element, main, footer, window; 9 | 10 | beforeEach(inject(function ($compile, $rootScope, $window) { 11 | var scope = $rootScope.$new(), 12 | elem = angular.element("
"); 13 | 14 | window = $window; 15 | directive = $compile(elem)(scope); 16 | scope.$digest(); 17 | element = elem.find('.draggable'); 18 | main = elem.find('main'); 19 | footer = elem.find('footer#journal-wrapper'); 20 | isoScope = element.isolateScope(); 21 | })); 22 | 23 | it('should add class "dragging" on mousedown', function() { 24 | expect(element.hasClass('dragging')).toBeFalsy(); 25 | element.find('.drag-handle').triggerHandler('mousedown'); 26 | expect(element.hasClass('dragging')).toBeTruthy(); 27 | }); 28 | 29 | it('should remove class "dragging" on mouseup', function() { 30 | element.addClass('dragging'); 31 | $('body').triggerHandler('mouseup'); 32 | expect(element.hasClass('dragging')).toBeFalsy(); 33 | }); 34 | 35 | describe('when mousemove', function() { 36 | beforeEach(function() { 37 | window.innerHeight = 1000; 38 | spyOn(isoScope, 'updatePosition'); 39 | }); 40 | 41 | it('should do nothing if not "dragging"', function() { 42 | $('body').triggerHandler({ 43 | type : 'mousemove', 44 | pageX: 0, 45 | pageY: 500 46 | }); 47 | 48 | expect(isoScope.updatePosition).not.toHaveBeenCalled(); 49 | }); 50 | 51 | it('should set main and footer size when "dragging"', function() { 52 | element.addClass('dragging'); 53 | 54 | $('body').triggerHandler({ 55 | type : 'mousemove', 56 | pageX: 0, 57 | pageY: 500 58 | }); 59 | 60 | expect(isoScope.updatePosition).toHaveBeenCalledWith(500); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/spec/directives/grid.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: grid', function () { 4 | // load the directive's module 5 | beforeEach(module('iLayers')); 6 | // Load the templates 7 | beforeEach(module('views/grid.html')); 8 | beforeEach(module('views/leaf.html')); 9 | 10 | var directive, scope, route, controller, layer, commandService, gridService; 11 | 12 | beforeEach(inject(function ($compile, $rootScope, $routeParams, _commandService_, _gridService_) { 13 | var elem = angular.element("
"); 14 | scope = $rootScope.$new(); 15 | scope.graph = []; 16 | scope.applyFilters = function() { return [] }; 17 | directive = $compile(elem)(scope); 18 | scope.$digest(); 19 | controller = elem.controller('grid'); 20 | layer = { Size: 0, container_config: { Cmd: [] } }; 21 | commandService = _commandService_; 22 | gridService = _gridService_; 23 | route = $routeParams; 24 | })); 25 | 26 | describe('classifyLayer', function() { 27 | it('should return noop when count is 0', function() { 28 | var classes = controller.classifyLayer(layer, 0); 29 | expect(classes).toEqual('noop'); 30 | }); 31 | 32 | it('should return command classes', function() { 33 | layer.container_config.Cmd = ['RUN ']; 34 | expect(controller.classifyLayer(layer, 1)).toEqual('box cat1'); 35 | layer.container_config.Cmd = ['ADD ']; 36 | expect(controller.classifyLayer(layer, 2)).toEqual('box cat2'); 37 | layer.container_config.Cmd = ['VOLUME ']; 38 | expect(controller.classifyLayer(layer, 1)).toEqual('box cat3'); 39 | layer.container_config.Cmd = ['CMD ']; 40 | expect(controller.classifyLayer(layer, 1)).toEqual('box cat4'); 41 | layer.container_config.Cmd = ['FROM ']; 42 | expect(controller.classifyLayer(layer, 1)).toEqual('box cat5'); 43 | }); 44 | }); 45 | 46 | describe('$scope.checkLockParam', function() { 47 | beforeEach(function() { 48 | spyOn(commandService, 'lock'); 49 | }); 50 | 51 | describe('when route has lock', function() { 52 | it('should call commandService.lock', function() { 53 | route.lock = 'test:foo'; 54 | scope.checkLockParam(); 55 | expect(commandService.lock).toHaveBeenCalledWith({ name: 'test', tag: 'foo' }); 56 | }); 57 | 58 | it('should add latest tag if not provided', function() { 59 | route.lock = 'test'; 60 | scope.checkLockParam(); 61 | expect(commandService.lock).toHaveBeenCalledWith({ name: 'test', tag: 'latest' }); 62 | }); 63 | }); 64 | 65 | describe('when route has no lock', function() { 66 | it('should not call commandService.lock', function() { 67 | scope.checkLockParam(); 68 | expect(commandService.lock).not.toHaveBeenCalled(); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('scope.$on("lock-image")', function() { 74 | beforeEach(function() { 75 | scope.graph = [{ repo: { name: 'foo', tag: 'foo' }, layers: [{ id: '1' }] }]; 76 | scope.grid = [{type: '', layer: { id: '1' } }]; 77 | }); 78 | 79 | it('should lock layers', function() { 80 | scope.$broadcast('lock-image', { image: { name: 'foo', tag: 'foo' } }); 81 | expect(scope.grid[0].type).toEqual(' locked'); 82 | }); 83 | 84 | it('should unlock layers when already locked', function() { 85 | scope.grid[0].type = 'locked'; 86 | scope.$broadcast('lock-image', {}); 87 | expect(scope.grid[0].type).toEqual(''); 88 | }); 89 | }); 90 | 91 | describe('findWidth', function() { 92 | it('should return 0 when count = 0', function() { 93 | expect(controller.findWidth(0)).toEqual(0); 94 | }); 95 | 96 | it('should return box width when count is 1', function() { 97 | expect(controller.findWidth(1)).toEqual(160); 98 | }); 99 | 100 | it('should return box width + 20 padding', function() { 101 | expect(controller.findWidth(2)).toEqual(360); 102 | }); 103 | }); 104 | 105 | describe('$scope.unwrapGrid', function() { 106 | it('creates an array from matrix data', function() { 107 | var data = { cols: 1, rows: 1, matrix: { map: [[{layer: { id: 'foo' } }]], 108 | inventory: {'foo': {image: { repo: { name: 'test' } } }, count: 1 } } }, 109 | res = gridService.findLeaves(data); 110 | 111 | expect(res.length).toEqual(1); 112 | }); 113 | }); 114 | 115 | describe('scope.highlightCommand', function() { 116 | }); 117 | 118 | describe('scope.clearCommand', function() { 119 | it('should call commandService.clear()', function() { 120 | spyOn(commandService, 'clear'); 121 | scope.clearCommands(); 122 | expect(commandService.clear).toHaveBeenCalled(); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/spec/directives/imagesearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: imageSearch', function () { 4 | // load the directive's module 5 | beforeEach(module('iLayers')); 6 | // Load the templates 7 | beforeEach(module('views/imageSearch.html')); 8 | 9 | var element, 10 | controller, 11 | directive, 12 | registryService, 13 | deferredTag, 14 | deferredSuccess, 15 | rootScope, 16 | scope; 17 | 18 | beforeEach(inject(function ($q, $compile, $rootScope, _registryService_) { 19 | var autoElem = angular.element("
"); 20 | 21 | rootScope = $rootScope.$new(); 22 | 23 | directive = $compile(autoElem)(rootScope); 24 | rootScope.$digest(); 25 | 26 | element = autoElem.find('[image-search]'); 27 | scope = element.isolateScope(); 28 | controller = element.controller('imageSearch'); 29 | 30 | registryService = _registryService_; 31 | })); 32 | 33 | it('should initialize tagList', function() { 34 | 35 | expect(scope.tagList.length).toEqual(0); 36 | }); 37 | 38 | it('should initialize autocomplete_options', function() { 39 | expect(scope.autocomplete_options).toEqual( 40 | { 41 | 'suggest': jasmine.any(Function), 42 | 'on_error': jasmine.any(Function), 43 | 'on_attach': jasmine.any(Function), 44 | 'on_select': jasmine.any(Function) 45 | }); 46 | }); 47 | 48 | describe('suggestImages', function() { 49 | beforeEach(inject(function($q) { 50 | deferredSuccess = $q.defer(); 51 | spyOn(registryService, 'search').and.returnValue(deferredSuccess.promise); 52 | scope.model = { name: 'test' }; 53 | })); 54 | 55 | it('should return empty array when term size < 3', function() { 56 | var list = scope.suggestImages('me'); 57 | expect(list.length).toEqual(0); 58 | }); 59 | 60 | it('calls registryService.search when term > 2', function() { 61 | deferredSuccess.resolve({ data: { results: [{ name: 'foo' },{ name: 'bar' }] } }); 62 | 63 | var list = scope.suggestImages('term'); 64 | expect(registryService.search).toHaveBeenCalledWith('term'); 65 | }); 66 | 67 | describe('when image is valid', function() { 68 | it('should remove missing', function() { 69 | deferredSuccess.resolve({ data: { results: [{ name: 'foo' },{ name: 'bar' }] } }); 70 | 71 | scope.suggestImages('xtermx'); 72 | 73 | scope.$apply(); 74 | 75 | expect(scope.model.missing).toBe(true); 76 | }); 77 | }); 78 | 79 | describe('when image is not in results', function() { 80 | it('should set missing flag', function() { 81 | expect(scope.model.missing).toBeFalsy(); 82 | deferredSuccess.resolve({ data: { results: [{ name: 'term/bar' },{ name: 'bar' }] } }); 83 | 84 | scope.suggestImages('term'); 85 | 86 | scope.$apply(); 87 | 88 | expect(scope.model.missing).toBeTruthy(); 89 | }); 90 | }); 91 | }); 92 | 93 | describe('$watch model', function() { 94 | it('should fetchTags when model intially loaded', inject(function($q) { 95 | var deferredTag = $q.defer(); 96 | spyOn(registryService, 'fetchTags').and.returnValue(deferredTag.promise); 97 | deferredTag.resolve({}); 98 | scope.initialValue = function() { return true; } 99 | scope.model = { name: 'one' } 100 | scope.withTags = true; 101 | scope.model.name = 'blah'; 102 | scope.$digest(); 103 | scope.model.name = 'blah'; 104 | scope.$digest(); 105 | expect(registryService.fetchTags).toHaveBeenCalled(); 106 | })); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/spec/directives/leaf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: leaf', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | // Load the templates 8 | beforeEach(module('views/leaf.html')); 9 | 10 | var directive, scope, controller, commandService, location, repo, elem; 11 | 12 | beforeEach(inject(function ($compile, $rootScope, $location, _commandService_) { 13 | elem = angular.element(""); 14 | commandService = _commandService_; 15 | location = $location; 16 | scope = $rootScope.$new(); 17 | scope.graph = []; 18 | directive = $compile(elem)(scope); 19 | scope.$digest(); 20 | controller = elem.controller('leaf'); 21 | })); 22 | 23 | describe('showCommands', function() { 24 | beforeEach(function() { 25 | scope.graph = [{ 'repo': { 'name': 'foo', 'tag': 'do'}, layers: ['one', 'two'] }]; 26 | spyOn(commandService, 'highlight'); 27 | }); 28 | 29 | it('should send all layers to commandService.highlight', function() { 30 | repo = { 'name': 'foo', 'tag': 'do'}; 31 | scope.showCommands(repo); 32 | expect(commandService.highlight).toHaveBeenCalledWith(['one','two'], undefined); 33 | }); 34 | 35 | it('should not call commandService if no image matches', function() { 36 | var repo = { 'name': 'boo', 'tag': 'hoo'}; 37 | scope.showCommands(repo); 38 | expect(commandService.highlight).not.toHaveBeenCalled(); 39 | }); 40 | }); 41 | 42 | describe('applyLock', function() { 43 | beforeEach(function() { 44 | spyOn(commandService, 'release'); 45 | repo = { identity: 'foo' }; 46 | }); 47 | 48 | describe('when locking', function() { 49 | beforeEach(function() { 50 | spyOn(commandService, 'lock').and.returnValue(undefined); 51 | }); 52 | 53 | it('sets lockParam', function() { 54 | scope.applyLock(repo); 55 | expect(scope.lockParam).toBeTruthy(); 56 | }); 57 | 58 | it('calls release on commandService', function() { 59 | scope.applyLock(repo); 60 | expect(commandService.release).toHaveBeenCalled(); 61 | }); 62 | 63 | it('calls lock on commandService', function() { 64 | scope.applyLock(repo); 65 | expect(commandService.lock).toHaveBeenCalledWith(repo); 66 | }); 67 | }) 68 | 69 | describe('when unlocking', function() { 70 | beforeEach(function() { 71 | elem.addClass('locked'); 72 | spyOn(commandService, 'lock').and.returnValue(repo); 73 | }); 74 | 75 | it('should call commandService.release', function() { 76 | scope.applyLock(repo); 77 | expect(commandService.release).toHaveBeenCalled(); 78 | }); 79 | }); 80 | 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/spec/directives/loadingsrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: loadingSrc', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | }); 15 | -------------------------------------------------------------------------------- /test/spec/directives/metrics.js: -------------------------------------------------------------------------------- 1 | describe('Metrics Directive', function() { 2 | // Load the module 3 | beforeEach(module('iLayers')); 4 | // Load the templates 5 | beforeEach(module('views/metrics.html')); 6 | 7 | var directive, scope, controller, layers; 8 | 9 | beforeEach(inject(function ($compile, $rootScope) { 10 | var elem = angular.element(""); 11 | scope = $rootScope.$new(); 12 | scope.graph = []; 13 | scope.applyFilters = function() { return [] }; 14 | directive = $compile(elem)(scope); 15 | scope.$digest(); 16 | controller = elem.controller('metrics'); 17 | })); 18 | 19 | it('should initialize metrics', function() { 20 | expect(angular 21 | .equals(scope.metrics, { count: 0, size: 0, ave: 0, largest:0 })) 22 | .toBeTruthy(); 23 | }); 24 | 25 | describe('calculateMetrics', function() { 26 | beforeEach(function() { 27 | spyOn(scope, "sequential"); 28 | 29 | layers = [{ layers: [ 30 | { id: 'foo', Size: 300 }, 31 | { id: 'baz', Size: 200 }, 32 | { id: 'bar', Size: 1000 } 33 | ]} 34 | ] 35 | }); 36 | 37 | it('should call sequential with the total layer count', function() { 38 | scope.calculateMetrics(layers); 39 | expect(scope.sequential).toHaveBeenCalledWith('count', 0, 3, 600); 40 | }); 41 | 42 | it('should call sequential with the total layer size', function() { 43 | scope.calculateMetrics(layers); 44 | expect(scope.sequential).toHaveBeenCalledWith('size', 0, 1500, 520); 45 | }); 46 | 47 | it('should call sequential with the layer average', function() { 48 | scope.calculateMetrics(layers); 49 | expect(scope.sequential).toHaveBeenCalledWith('ave', 0, 500, 520); 50 | }); 51 | 52 | it('should call sequential with the largest layer size', function() { 53 | scope.calculateMetrics(layers); 54 | expect(scope.sequential).toHaveBeenCalledWith('largest', 0, 1000, 520); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/spec/directives/notification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: notification', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | }); 15 | -------------------------------------------------------------------------------- /test/spec/directives/resetfield.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: resetField', function () { 4 | // load the directive's module 5 | beforeEach(module('iLayers')); 6 | 7 | var directive, scope, elem, btn; 8 | 9 | beforeEach(inject(function ($compile, $rootScope) { 10 | var tmpl = angular.element("
"); 11 | scope = $rootScope.$new(); 12 | scope.filter = ""; 13 | scope.graph = []; 14 | directive = $compile(tmpl)(scope); 15 | scope.$digest(); 16 | elem = tmpl.find('input'); 17 | btn = tmpl.find('.reset-icon'); 18 | })); 19 | 20 | describe('compiling the directive', function() 21 | { 22 | it('should throw an error if a model is absent', function() { 23 | function template() { 24 | return $compile('')(scope); 25 | } 26 | expect(template).toThrow(); 27 | }); 28 | }); 29 | 30 | it('should show the icon when the field has a value', function() { 31 | expect(btn.hasClass('hidden')).toBeTruthy(); 32 | elem.val('test'); 33 | elem.triggerHandler('focus'); 34 | expect(btn.hasClass('hidden')).toBeFalsy(); 35 | }); 36 | 37 | it('should hide the icon when the field is empty', function() { 38 | elem.val(''); 39 | elem.triggerHandler('focus'); 40 | expect(btn.hasClass('hidden')).toBeTruthy(); 41 | 42 | }); 43 | 44 | it('should clear the field when clicked', function() { 45 | elem.val('testing'); 46 | btn.click(); 47 | expect(elem.val() === '').toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/spec/directives/sticky.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: sticky', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | 8 | var directive, scope, element; 9 | 10 | beforeEach(inject(function ($compile, $rootScope) { 11 | element = angular.element('
'); 12 | scope = $rootScope.$new(); 13 | directive = $compile(element)(scope); 14 | scope.$digest(); 15 | })); 16 | 17 | /* This doesn't appear to work in PhantomJS */ 18 | describe('when scrolling', function() { 19 | it('should add sticky class to element', function() { 20 | //element.('main').scrollTop(50); 21 | //element.parent('main').triggerHandler('scroll'); 22 | 23 | //expect(element.hasClass('sticky')).toBeTruthy(); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/directives/syncScroll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: syncScroll', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('iLayers')); 7 | 8 | var directive, scope, element; 9 | 10 | beforeEach(inject(function ($compile, $rootScope) { 11 | element = angular.element('
'); 12 | scope = $rootScope.$new(); 13 | directive = $compile(element)(scope); 14 | scope.$digest(); 15 | })); 16 | 17 | /* This doesn't appear to work in PhantomJS */ 18 | describe('when scrolling', function() { 19 | it('should add sync-scroll class to element', function() { 20 | //element.scrollTop(50); 21 | //element.find('.graph').scrollLeft(50); 22 | //element.parent('main').triggerHandler('scroll'); 23 | //expect(element.find('.test').css('left')).toEqual(50); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/spec/filters/size.js: -------------------------------------------------------------------------------- 1 | describe('Size Filter', function() { 2 | // Load the module 3 | beforeEach(module('iLayers')); 4 | 5 | var filter; 6 | 7 | beforeEach(inject(function ($filter) { 8 | filter = $filter; 9 | })); 10 | 11 | it('should convert 0 to 0 Bytes', function() { 12 | var result = filter('size')(0); 13 | expect(result.$$unwrapTrustedValue()).toEqual('0 Bytes'); 14 | }); 15 | 16 | it('should convert 1 to 1 byte', function() { 17 | var result = filter('size')(1); 18 | expect(result.$$unwrapTrustedValue()).toEqual('1 Byte'); 19 | }); 20 | 21 | it('should convert 10 to 10 bytes', function() { 22 | var result = filter('size')(10); 23 | expect(result.$$unwrapTrustedValue()).toEqual('10 Bytes'); 24 | }); 25 | 26 | it('should convert 1024 to 1 KiB', function() { 27 | var result = filter('size')(1024); 28 | expect(result.$$unwrapTrustedValue()).toEqual('1 KiB'); 29 | }); 30 | 31 | it('should convert 1,048,576 to 1 MiB', function() { 32 | var result = filter('size')(1048576); 33 | expect(result.$$unwrapTrustedValue()).toEqual('1 MiB'); 34 | }); 35 | 36 | it('should convert 1,073,741,824 to 1 GiB', function() { 37 | var result = filter('size')(1073741824); 38 | expect(result.$$unwrapTrustedValue()).toEqual('1 GiB'); 39 | }); 40 | 41 | it('should convert 1,099,511,627,776 to 1 TiB', function() { 42 | var result = filter('size')(1099511627776); 43 | expect(result.$$unwrapTrustedValue()).toEqual('1 TiB'); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/spec/services/commandservice.js: -------------------------------------------------------------------------------- 1 | describe('Command Service', function() { 2 | // Load the module 3 | beforeEach(module('iLayers')); 4 | 5 | var service, rootScope; 6 | 7 | beforeEach(inject(function ($rootScope, _commandService_) { 8 | service = _commandService_; 9 | rootScope = $rootScope; 10 | spyOn(rootScope, '$broadcast'); 11 | })); 12 | 13 | describe('constructCommand', function (){ 14 | it('should transform port mappings', function() { 15 | var result = service.constructCommand('#(nop) EXPOSE map[9292/tcp:{}]'); 16 | expect(result) 17 | .toEqual('EXPOSE 9292'); 18 | }); 19 | 20 | it('should transform command brackets', function() { 21 | var result = service.constructCommand('#(nop) CMD [test]'); 22 | expect(result) 23 | .toEqual('CMD test'); 24 | }); 25 | it('should set an undefined command to empty string', function() { 26 | var result = service.constructCommand(undefined, 0); 27 | expect(result) 28 | .toEqual(''); 29 | }); 30 | it('should set a null command to FROM scratch', function() { 31 | var result = service.constructCommand(null, 0); 32 | expect(result) 33 | .toEqual('FROM scratch'); 34 | }); 35 | it('should set a null command on an image with a size greater than 0 to unknown instruction', function() { 36 | var result = service.constructCommand(null, 1); 37 | expect(result) 38 | .toEqual('unknown instruction'); 39 | }); 40 | }); 41 | 42 | describe('highlight', function() { 43 | it('should broadcast "command-change" event with commands', function() { 44 | service.highlight([{ container_config: { Cmd: ['RUN this thing'] } }]); 45 | expect(rootScope.$broadcast) 46 | .toHaveBeenCalledWith('command-change', {'commands': ['RUN this thing']}); 47 | }); 48 | 49 | }); 50 | 51 | describe('clear', function() { 52 | it('should broadcast "command-change" event with no commands', function() { 53 | service.clear(); 54 | expect(rootScope.$broadcast) 55 | .toHaveBeenCalledWith('command-change', {'commands': []}); 56 | }); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /test/spec/services/errorinterceptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: errorInterceptor', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('iLayers')); 7 | 8 | // instantiate service 9 | var errorInterceptor, errorService; 10 | 11 | beforeEach(inject(function (_errorInterceptor_, _errorService_) { 12 | errorInterceptor = _errorInterceptor_; 13 | errorService = _errorService_; 14 | spyOn(errorService, 'error'); 15 | })); 16 | 17 | describe('when successful', function() { 18 | it('should return unchanged response', function() { 19 | var sample = 'testing', 20 | result = errorInterceptor.response(sample); 21 | 22 | expect(sample).toEqual(result); 23 | }); 24 | }); 25 | 26 | describe('when error', function() { 27 | it('should call error service with default string', function() { 28 | var msg = 'Unable to communicate to ImageLayers Services'; 29 | errorInterceptor.responseError({}) 30 | 31 | expect(errorService.error).toHaveBeenCalledWith(msg); 32 | }); 33 | 34 | it('should call error with response message', function() { 35 | var msg = 'Testing'; 36 | errorInterceptor.responseError({ data: msg }); 37 | 38 | expect(errorService.error).toHaveBeenCalledWith(msg); 39 | }); 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/services/errorservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: errorService', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('iLayers')); 7 | 8 | // instantiate service 9 | var errorService; 10 | beforeEach(inject(function (_errorService_) { 11 | errorService = _errorService_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!errorService).toBe(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/services/gridservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: gridService', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('iLayers')); 7 | 8 | // instantiate service 9 | var gridService, data; 10 | 11 | beforeEach(inject(function (_gridService_) { 12 | gridService = _gridService_; 13 | })); 14 | 15 | describe('buildGrid', function() { 16 | describe('when no images are defined', function() { 17 | beforeEach(function() { 18 | data = []; 19 | }); 20 | 21 | it('should have rows = 0', function() { 22 | var res = gridService.buildGrid(data); 23 | expect(res.rows).toEqual(0); 24 | }); 25 | 26 | it('should have cols = 0', function() { 27 | var res = gridService.buildGrid(data); 28 | expect(res.cols).toEqual(0); 29 | }); 30 | 31 | it('should return empty matrix', function() { 32 | var res = gridService.buildGrid(data); 33 | expect(res.matrix).toEqual({ inventory: {}, map: [] }); 34 | }); 35 | }); 36 | 37 | describe('when images are defined', function() { 38 | beforeEach(function() { 39 | data = [ 40 | { layers: [{id: 'baz', 'parent': 'bar' }, { id: 'bar', 'parent': 'foo' }, { id: 'foo', 'parent': '' }] }, 41 | { layers: [{ id: 'bak', 'parent': 'foo' }, { id: 'foo', 'parent': '' }] }, 42 | { layers: [{ id: 'boo', 'parent': 'bar' }, { id: 'bar', 'parent': 'foo' }, { id: 'foo', 'parent': '' }] } 43 | ]; 44 | }); 45 | 46 | it('should eliminate images without layers', function() { 47 | var res = ''; 48 | 49 | data.push({ layers: [] }); 50 | 51 | expect(data.length).toEqual(4); 52 | res = gridService.buildGrid(data); 53 | expect(res.cols).toEqual(3); 54 | }); 55 | 56 | it('should have rows = 3', function() { 57 | var res = gridService.buildGrid(data); 58 | expect(res.rows).toEqual(3); 59 | }); 60 | 61 | it('should have cols = 3', function() { 62 | var res = gridService.buildGrid(data); 63 | expect(res.cols).toEqual(3); 64 | }); 65 | 66 | it('should return a foo location data', function() { 67 | var res = gridService.buildGrid(data); 68 | expect(res.matrix.inventory.foo.row).toEqual(2); 69 | expect(res.matrix.inventory.foo.count).toEqual(3); 70 | }); 71 | 72 | it('should return a bar location data', function() { 73 | var res = gridService.buildGrid(data); 74 | expect(res.matrix.inventory.bar.row).toEqual(1); 75 | expect(res.matrix.inventory.bar.count).toEqual(1); 76 | }); 77 | 78 | it('should return a baz location data', function() { 79 | var res = gridService.buildGrid(data); 80 | expect(res.matrix.inventory.baz.row).toEqual(0); 81 | expect(res.matrix.inventory.baz.count).toEqual(0); 82 | }); 83 | 84 | it('should return a boo location data', function() { 85 | var res = gridService.buildGrid(data); 86 | expect(res.matrix.inventory.boo.row).toEqual(0); 87 | expect(res.matrix.inventory.boo.count).toEqual(1); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('findLeaves', function() { 93 | it('creates an array of repo items', function() { 94 | var data = { 95 | cols: 1,rows: 1, matrix: { 96 | map: [[{layer: { id: 'foo' } }]], 97 | inventory: {'foo': {image: { repo: { name: 'test' } } } } 98 | } 99 | }, 100 | res = gridService.findLeaves(data); 101 | 102 | expect(res.length).toEqual(1); 103 | }); 104 | it('creates a valid docker hub link for official images', function(){ 105 | var data = { 106 | cols: 1,rows: 1, matrix: { 107 | map: [[{layer: { id: 'foo' } }]], 108 | inventory: {'foo': {image: { repo: { name: 'official' } } } } 109 | } 110 | }, 111 | res = gridService.findLeaves(data); 112 | expect(res[0].hub_link).toEqual('https://hub.docker.com/_/official'); 113 | }); 114 | it('creates a valid docker hub link for images from individuals and organizations', function () { 115 | var data = { 116 | cols: 1,rows: 1, matrix: { 117 | map: [[{layer: { id: 'foo' } }]], 118 | inventory: {'foo': {image: { repo: { name: 'foo/test' } } } } 119 | } 120 | }, 121 | res = gridService.findLeaves(data); 122 | expect(res[0].hub_link).toEqual('https://hub.docker.com/r/foo/test'); 123 | }); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/spec/services/registryservice.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: registryService', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('iLayers')); 7 | 8 | // instantiate service 9 | var httpBackend, requestHandler, env, registryService, errorService; 10 | 11 | beforeEach(inject(function ($httpBackend, _ENV_, _registryService_, _errorService_) { 12 | registryService = _registryService_; 13 | errorService = _errorService_; 14 | httpBackend = $httpBackend; 15 | env = _ENV_; 16 | env.apiEndpoint = 'http://localhost'; 17 | })); 18 | 19 | afterEach(function() { 20 | httpBackend.verifyNoOutstandingExpectation(); 21 | httpBackend.verifyNoOutstandingRequest(); 22 | }); 23 | 24 | describe('inspect', function() { 25 | beforeEach(function() { 26 | requestHandler = httpBackend.whenPOST('http://localhost/registry/analyze'); 27 | requestHandler.respond(200, [{ layers: [] }]); 28 | spyOn(errorService, 'notification'); 29 | }); 30 | 31 | it('should return list of images', function() { 32 | httpBackend.expectPOST('http://localhost/registry/analyze'); 33 | registryService.inspect([]); 34 | httpBackend.flush(); 35 | }); 36 | 37 | it('should call errorService.notification when image not found', function() { 38 | requestHandler.respond(200, [{ layers: null, repo: { name: 'foo', tag: 'bar' } }]); 39 | httpBackend.expectPOST('http://localhost/registry/analyze'); 40 | registryService.inspect([]); 41 | httpBackend.flush(); 42 | 43 | expect(errorService.notification).toHaveBeenCalled(); 44 | }); 45 | 46 | }); 47 | 48 | describe('search', function() { 49 | beforeEach(function() { 50 | requestHandler = httpBackend.whenGET('http://localhost/registry/search?name=foo'); 51 | requestHandler.respond(200, ''); 52 | }); 53 | 54 | it('should call search url', function() { 55 | httpBackend.expectGET('http://localhost/registry/search?name=foo'); 56 | registryService.search('foo'); 57 | httpBackend.flush(); 58 | }); 59 | }); 60 | 61 | describe('fetchTags', function() { 62 | beforeEach(function() { 63 | requestHandler = httpBackend.whenGET('http://localhost/registry/images/foo/tags'); 64 | requestHandler.respond(200, ''); 65 | }); 66 | 67 | it('should call tags url', function() { 68 | httpBackend.expectGET('http://localhost/registry/images/foo/tags'); 69 | registryService.fetchTags('foo'); 70 | httpBackend.flush(); 71 | }); 72 | }); 73 | }); 74 | --------------------------------------------------------------------------------