Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group by for images #823

Merged
merged 10 commits into from Mar 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions commands/push-image.ts
Expand Up @@ -10,7 +10,7 @@ import { ImageNode } from '../explorer/models/imageNode';
import { RootNode } from '../explorer/models/rootNode';
import { ext } from '../extensionVariables';
import { askToSaveRegistryPath } from './registrySettings';
import { addImageTaggingTelemetry, getOrAskForImageAndTag, IHasImageDescriptorAndLabel, tagImage } from './tag-image';
import { addImageTaggingTelemetry, getOrAskForImageAndTag, IHasImageDescriptorAndFullTag, tagImage } from './tag-image';

export async function pushImage(actionContext: IActionContext, context: ImageNode | RootNode | undefined): Promise<void> {
let properties: {
Expand Down Expand Up @@ -39,7 +39,7 @@ export async function pushImage(actionContext: IActionContext, context: ImageNod
// ext.context.workspaceState.update(addPrefixImagePush, false);
// }
if (response === tagFirst) {
imageName = await tagImage(actionContext, <IHasImageDescriptorAndLabel>{ imageDesc: imageToPush, label: imageName }); //not passing this would ask the user a second time to pick an image
imageName = await tagImage(actionContext, <IHasImageDescriptorAndFullTag>{ imageDesc: imageToPush, fullTag: imageName }); //not passing this would ask the user a second time to pick an image
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions commands/tag-image.ts
Expand Up @@ -14,7 +14,7 @@ import { extractRegExGroups } from '../helpers/extractRegExGroups';
import { docker } from './utils/docker-endpoint';
import { ImageItem, quickPickImage } from './utils/quick-pick-image';

export async function tagImage(actionContext: IActionContext, context: ImageNode | RootNode | IHasImageDescriptorAndLabel | undefined): Promise<string> {
export async function tagImage(actionContext: IActionContext, context: ImageNode | RootNode | IHasImageDescriptorAndFullTag | undefined): Promise<string> {
// If a RootNode or no node is passed in, we ask the user to pick an image
let [imageToTag, currentName] = await getOrAskForImageAndTag(actionContext, context instanceof RootNode ? undefined : context);

Expand Down Expand Up @@ -68,18 +68,18 @@ export async function getTagFromUserInput(imageName: string, addDefaultRegistry:
return nameWithTag;
}

export interface IHasImageDescriptorAndLabel {
imageDesc: Docker.ImageDesc,
label: string
export interface IHasImageDescriptorAndFullTag {
imageDesc: Docker.ImageDesc;
fullTag: string;
}

export async function getOrAskForImageAndTag(actionContext: IActionContext, context: IHasImageDescriptorAndLabel | undefined): Promise<[Docker.ImageDesc, string]> {
export async function getOrAskForImageAndTag(actionContext: IActionContext, context: IHasImageDescriptorAndFullTag | undefined): Promise<[Docker.ImageDesc, string]> {
let name: string;
let description: Docker.ImageDesc;

if (context && context.imageDesc) {
description = context.imageDesc;
name = context.label;
name = context.fullTag;
} else {
const selectedItem: ImageItem = await quickPickImage(actionContext, false);
if (selectedItem) {
Expand Down
4 changes: 2 additions & 2 deletions commands/utils/quick-pick-image.ts
Expand Up @@ -59,7 +59,7 @@ function computeItems(images: Docker.ImageDesc[], includeAll?: boolean): ImageIt
export async function quickPickImage(actionContext: IActionContext, includeAll?: boolean): Promise<ImageItem> {
let images: Docker.ImageDesc[];
let properties: {
allImages?: boolean;
allImages?: string;
} & TelemetryProperties = actionContext.properties;

const imageFilters = {
Expand All @@ -78,7 +78,7 @@ export async function quickPickImage(actionContext: IActionContext, includeAll?:
} else {
const items: ImageItem[] = computeItems(images, includeAll);
let response = await ext.ui.showQuickPick<ImageItem>(items, { placeHolder: 'Choose image...' });
properties.allContainers = includeAll ? String(response.allImages) : undefined;
properties.allImages = includeAll ? String(response.allImages) : undefined;
return response;
}
}
Expand Down
3 changes: 3 additions & 0 deletions constants.ts
Expand Up @@ -5,6 +5,8 @@

import * as path from 'path';

export const configPrefix: string = 'docker';

export const imagesPath: string = path.join(__dirname, '../images');

//AsyncPool Constants
Expand All @@ -24,6 +26,7 @@ export namespace keytarConstants {

export namespace configurationKeys {
export const defaultRegistryPath = "defaultRegistryPath";
export const groupImagesBy = 'groupImagesBy';
}

//Credentials Constants
Expand Down
File renamed without changes.
7 changes: 1 addition & 6 deletions explorer/models/containerNode.ts
Expand Up @@ -4,8 +4,6 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { trimWithElipsis } from '../utils/utils';
import { getImageOrContainerDisplayName } from './getImageOrContainerDisplayName';
import { IconPath, NodeBase } from './nodeBase';

export type ContainerNodeContextValue = 'stoppedLocalContainerNode' | 'runningLocalContainerNode';
Expand All @@ -22,11 +20,8 @@ export class ContainerNode extends NodeBase {
}

public getTreeItem(): vscode.TreeItem {
let config = vscode.workspace.getConfiguration('docker');
let displayName: string = getImageOrContainerDisplayName(this.label, config.get('truncateLongRegistryPaths'), config.get('truncateMaxLength'));

return {
label: `${displayName}`,
label: this.label,
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextValue: this.contextValue,
iconPath: this.iconPath
Expand Down
18 changes: 18 additions & 0 deletions explorer/models/getContainerLabel.ts
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ContainerDesc } from 'dockerode';

export function getContainerLabel(container: ContainerDesc, labelTemplate: string): string {
let image = container.Image;
let name = container.Names[0].substr(1); // Remove start '/'
let status = container.Status;

let label = labelTemplate
.replace('{image}', image)
.replace('{name}', name)
.replace('{status}', status);
return label;
}
94 changes: 94 additions & 0 deletions explorer/models/getImageLabel.ts
@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import { ContainerDesc, ImageDesc } from 'dockerode';
import * as moment from 'moment';
import * as vscode from 'vscode';
import { extractRegExGroups } from '../../helpers/extractRegExGroups';
import { trimWithElipsis } from '../utils/utils';

// If options not specified, retrieves them from user settings
export function getImageLabel(fullTag: string, image: ImageDesc, labelTemplate: string, options?: { truncateLongRegistryPaths: boolean, truncateMaxLength: number }): string {
let truncatedRepository = truncate(getRepository(fullTag), options);
let repositoryName = getRepositoryName(fullTag);
let tag = getTag(fullTag);
let truncatedFullTag = truncate(fullTag, options);
let createdSince = getCreatedSince(image.Created || 0);
let imageId = (image.Id || '').replace('sha256:', '');
let shortImageId = imageId.slice(0, 12);

let label = labelTemplate
.replace('{repository}', truncatedRepository)
.replace('{repositoryName}', repositoryName)
.replace('{tag}', tag)
.replace('{fullTag}', truncatedFullTag)
.replace('{createdSince}', createdSince)
.replace('{shortImageId}', shortImageId);
assert(!label.match(/{|}/), "Unreplaced token");

return label;
}

/**
* Retrieves the full repository name
* @param fullTag [hostname/][username/]repositoryname[:tag]
* @returns [hostname/][username/]repositoryname
*/
function getRepository(fullTag: string): string {
let n = fullTag.lastIndexOf(':');
return n > 0 ? fullTag.slice(0, n) : fullTag;
}

function truncate(partialTag: string, options: { truncateLongRegistryPaths: boolean, truncateMaxLength: number } | undefined): string {
// Truncate if user desires
if (!options) {
let config = vscode.workspace.getConfiguration('docker');
let truncateLongRegistryPaths = config.get<boolean>('truncateLongRegistryPaths');
let truncateMaxLength = config.get<number>('truncateMaxLength');
options = {
truncateLongRegistryPaths: typeof truncateLongRegistryPaths === "boolean" ? truncateLongRegistryPaths : false,
truncateMaxLength: typeof truncateMaxLength === 'number' ? truncateMaxLength : 10
}
}

if (!options.truncateLongRegistryPaths) {
return partialTag;
}

// Extract registry from the rest of the name
let [registry, restOfName] = extractRegExGroups(partialTag, /^([^\/]+)\/(.*)$/, ['', partialTag]);

if (registry) {
let trimmedRegistry = trimWithElipsis(registry, options.truncateMaxLength);
return `${trimmedRegistry}/${restOfName}`;
}

return partialTag;
}

/**
* Retrieves just the name of the repository
* @param fullTag [hostname/][username/]repositoryname[:tag]
* @returns repositoryname
*/
function getRepositoryName(fullTag: string): string {
return fullTag.replace(/.*\//, "")
.replace(/:.*/, "");
}

/**
* Retrieves just the tag (without colon)
* @param fullTag [hostname/][username/]repositoryname[:tag]
* @returns tag
*/
function getTag(fullTag: string): string {
let n = fullTag.lastIndexOf(':');
return n > 0 ? fullTag.slice(n + 1) : '';
}

function getCreatedSince(created: number): string {
return moment(new Date(created * 1000)).fromNow();
}
25 changes: 0 additions & 25 deletions explorer/models/getImageOrContainerDisplayName.ts

This file was deleted.

40 changes: 40 additions & 0 deletions explorer/models/imageGroupNode.ts
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as vscode from 'vscode';
import { imagesPath } from '../../constants';
import { ImageNode } from './imageNode';
import { NodeBase } from './nodeBase';

export class ImageGroupNode extends NodeBase {

constructor(
public readonly label: string
) {
super(label);
}

public static readonly contextValue: string = 'localImageGroupNode';
public readonly contextValue: string = ImageGroupNode.contextValue;

public readonly children: ImageNode[] = [];

public getTreeItem(): vscode.TreeItem {
return {
label: this.label,
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: this.contextValue,
iconPath: {
light: path.join(imagesPath, 'light', 'application.svg'),
dark: path.join(imagesPath, 'dark', 'application.svg')
}
}
}

public async getChildren(): Promise<ImageNode[]> {
return this.children;
}
}
28 changes: 7 additions & 21 deletions explorer/models/imageNode.ts
Expand Up @@ -3,41 +3,27 @@
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as moment from 'moment';
import * as path from 'path';
import * as vscode from 'vscode';
import { imagesPath } from '../../constants';
import { trimWithElipsis } from '../utils/utils';
import { getImageOrContainerDisplayName } from './getImageOrContainerDisplayName';
import { getImageLabel } from './getImageLabel';
import { NodeBase } from './nodeBase';

export class ImageNode extends NodeBase {

constructor(
public readonly label: string,
public imageDesc: Docker.ImageDesc,
public readonly eventEmitter: vscode.EventEmitter<NodeBase>
public readonly fullTag: string,
public readonly imageDesc: Docker.ImageDesc,
public readonly labelTemplate: string
) {
super(label)
super(getImageLabel(fullTag, imageDesc, labelTemplate));
}

public static readonly contextValue: string = 'localImageNode';
public readonly contextValue: string = ImageNode.contextValue;

public getTreeItem(): vscode.TreeItem {
let config = vscode.workspace.getConfiguration('docker');
let displayName: string = getImageOrContainerDisplayName(this.label, config.get('truncateLongRegistryPaths'), config.get('truncateMaxLength'));

displayName = `${displayName} (${moment(new Date(this.imageDesc.Created * 1000)).fromNow()})`;

return {
label: `${displayName}`,
label: this.label,
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextValue: "localImageNode",
iconPath: {
light: path.join(imagesPath, 'light', 'application.svg'),
dark: path.join(imagesPath, 'dark', 'application.svg')
}
contextValue: this.contextValue
}
}

Expand Down