feat(docker/images): show used tag correctly [EE-5396] (#10305)

pull/10410/head
Chaim Lev-Ari 1 year ago committed by GitHub
parent b895e88075
commit 9bf2957ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,6 +8,7 @@ import (
"github.com/portainer/portainer/api/docker"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/handler/docker/containers"
"github.com/portainer/portainer/api/http/handler/docker/images"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
@ -45,6 +46,9 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dataStore, dockerClientFactory, containerService)
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
imagesHandler := images.NewHandler("/{id}/images", bouncer, dockerClientFactory)
endpointRouter.PathPrefix("/images").Handler(imagesHandler)
return h
}

@ -0,0 +1,32 @@
package images
import (
"net/http"
"github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/gorilla/mux"
)
type Handler struct {
*mux.Router
dockerClientFactory *client.ClientFactory
bouncer security.BouncerService
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(routePrefix string, bouncer security.BouncerService, dockerClientFactory *client.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dockerClientFactory: dockerClientFactory,
bouncer: bouncer,
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess)
router.Handle("", httperror.LoggerHandler(h.imagesList)).Methods(http.MethodGet)
return h
}

@ -0,0 +1,79 @@
package images
import (
"net/http"
"github.com/docker/docker/api/types"
"github.com/portainer/portainer/api/http/handler/docker/utils"
"github.com/portainer/portainer/api/internal/set"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
)
type ImageResponse struct {
Created int64 `json:"created"`
NodeName string `json:"nodeName"`
ID string `json:"id"`
Size int64 `json:"size"`
Tags []string `json:"tags"`
// Used is true if the image is used by at least one container
// supplied only when withUsage is true
Used bool `json:"used"`
}
// @id dockerImagesList
// @summary Fetch images
// @description
// @description **Access policy**:
// @tags docker
// @security jwt
// @param environmentId path int true "Environment identifier"
// @param withUsage query boolean false "Include image usage information"
// @produce json
// @success 200 {array} ImageResponse "Success"
// @failure 400 "Bad request"
// @failure 500 "Internal server error"
// @router /docker/{environmentId}/images [get]
func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
cli, httpErr := utils.GetClient(r, handler.dockerClientFactory)
if httpErr != nil {
return httpErr
}
images, err := cli.ImageList(r.Context(), types.ImageListOptions{})
if err != nil {
return httperror.InternalServerError("Unable to retrieve Docker images", err)
}
withUsage, err := request.RetrieveBooleanQueryParameter(r, "withUsage", true)
if err != nil {
return httperror.BadRequest("Invalid query parameter: withUsage", err)
}
imageUsageSet := set.Set[string]{}
if withUsage {
containers, err := cli.ContainerList(r.Context(), types.ContainerListOptions{})
if err != nil {
return httperror.InternalServerError("Unable to retrieve Docker containers", err)
}
for _, container := range containers {
imageUsageSet.Add(container.ImageID)
}
}
imagesList := make([]ImageResponse, len(images))
for i, image := range images {
imagesList[i] = ImageResponse{
Created: image.Created,
ID: image.ID,
Size: image.Size,
Tags: image.RepoTags,
Used: imageUsageSet.Contains(image.ID),
}
}
return response.JSON(w, imagesList)
}

@ -0,0 +1,28 @@
package utils
import (
"net/http"
dockerclient "github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
prclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/http/middlewares"
httperror "github.com/portainer/portainer/pkg/libhttp/error"
)
// GetClient returns a Docker client based on the request context
func GetClient(r *http.Request, dockerClientFactory *prclient.ClientFactory) (*dockerclient.Client, *httperror.HandlerError) {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return nil, httperror.NotFound("Unable to find an environment on request context", err)
}
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
cli, err := dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
if err != nil {
return nil, httperror.InternalServerError("Unable to connect to the Docker daemon", err)
}
return cli, nil
}

@ -14,12 +14,17 @@ function ImageHelperFactory() {
return tag.match(/^(?![\.\-])([a-zA-Z0-9\_\.\-])+$/g);
}
/**
*
* @param {import('@/react/docker/images/queries/useImages').ImagesListResponse[]} images
* @returns {{names: string[]}}}
*/
function getImagesNamesForDownload(images) {
var names = images.map(function (image) {
return image.RepoTags[0] !== '<none>:<none>' ? image.RepoTags[0] : image.Id;
return image.tags[0] !== '<none>:<none>' ? image.tags[0] : image.id;
});
return {
names: names,
names,
};
}

@ -74,13 +74,10 @@ const ngModule = angular
.component(
'dockerImagesDatatable',
r2a(withUIRouter(withCurrentUser(ImagesDatatable)), [
'dataset',
'environment',
'onRemove',
'isExportInProgress',
'isHostColumnVisible',
'onDownload',
'onRefresh',
'onRemove',
])
)

@ -46,15 +46,9 @@
</div>
<docker-images-datatable
ng-if="images"
dataset="images"
is-host-column-visible="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
on-download="(downloadAction)"
on-remove="(confirmRemovalAction)"
on-refresh="(getImages)"
is-export-in-progress="state.exportInProgress"
storage-key="images"
environment="endpoint"
settings-store="settingsStore"
containers="containers"
></docker-images-datatable>

@ -71,6 +71,11 @@ angular.module('portainer.docker').controller('ImagesController', [
});
}
/**
*
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} selectedItems
* @param {boolean} force
*/
$scope.confirmRemovalAction = async function (selectedItems, force) {
const confirmed = await (force ? confirmImageForceRemoval() : confirmRegularRemove());
@ -81,11 +86,15 @@ angular.module('portainer.docker').controller('ImagesController', [
$scope.removeAction(selectedItems, force);
};
/**
*
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} selectedItems
*/
function isAuthorizedToDownload(selectedItems) {
for (var i = 0; i < selectedItems.length; i++) {
var image = selectedItems[i];
var untagged = _.find(image.RepoTags, function (item) {
var untagged = _.find(image.tags, function (item) {
return item.indexOf('<none>') > -1;
});
@ -103,8 +112,12 @@ angular.module('portainer.docker').controller('ImagesController', [
return true;
}
/**
*
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} images
*/
function exportImages(images) {
HttpRequestHelper.setPortainerAgentTargetHeader(images[0].NodeName);
HttpRequestHelper.setPortainerAgentTargetHeader(images[0].nodeName);
$scope.state.exportInProgress = true;
ImageService.downloadImages(images)
.then(function success(data) {
@ -120,6 +133,10 @@ angular.module('portainer.docker').controller('ImagesController', [
});
}
/**
*
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} selectedItems
*/
$scope.downloadAction = function (selectedItems) {
if (!isAuthorizedToDownload(selectedItems)) {
return;
@ -133,15 +150,20 @@ angular.module('portainer.docker').controller('ImagesController', [
});
};
$scope.removeAction = function (selectedItems, force) {
$scope.removeAction = removeAction;
/**
*
* @param {Array<import('@/react/docker/images/queries/useImages').ImagesListResponse>} selectedItems
* @param {boolean} force
*/
function removeAction(selectedItems, force) {
var actionCount = selectedItems.length;
angular.forEach(selectedItems, function (image) {
HttpRequestHelper.setPortainerAgentTargetHeader(image.NodeName);
ImageService.deleteImage(image.Id, force)
HttpRequestHelper.setPortainerAgentTargetHeader(image.nodeName);
ImageService.deleteImage(image.id, force)
.then(function success() {
Notifications.success('Image successfully removed', image.Id);
var index = $scope.images.indexOf(image);
$scope.images.splice(index, 1);
Notifications.success('Image successfully removed', image.id);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to remove image');
@ -153,29 +175,11 @@ angular.module('portainer.docker').controller('ImagesController', [
}
});
});
};
$scope.getImages = getImages;
function getImages() {
ImageService.images(true)
.then(function success(data) {
$scope.images = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve images');
$scope.images = [];
});
}
$scope.setPullImageValidity = setPullImageValidity;
function setPullImageValidity(validity) {
$scope.state.pullRateValid = validity;
}
function initView() {
getImages();
}
initView();
},
]);

@ -3,7 +3,7 @@ import _ from 'lodash';
import { useMemo } from 'react';
import DockerIcon from '@/assets/ico/vendor/docker.svg?c';
import { useImages } from '@/react/docker/images/queries/useImages';
import { useImages } from '@/react/docker/proxy/queries/images/useImages';
import {
imageContainsURL,
getUniqueTagListFromImages,

@ -1,17 +1,14 @@
import { useQuery } from 'react-query';
import { Loader } from 'lucide-react';
import {
getContainerImagesStatus,
getServiceImagesStatus,
} from '@/react/docker/images/image.service';
import { useEnvironment } from '@/react/portainer/environments/queries';
import { statusIcon } from '@/react/docker/components/ImageStatus/helpers';
import { ResourceID, ResourceType } from '@/react/docker/images/types';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Icon } from '@@/Icon';
import { ResourceID, ResourceType } from './types';
import { useImageNotification } from './useImageNotification';
export interface Props {
environmentId: EnvironmentId;
resourceId: ResourceID;
@ -56,30 +53,3 @@ export function ImageStatus({
<Icon icon={statusIcon(data)} size="sm" className="!mr-1 align-middle" />
);
}
export function useImageNotification(
environmentId: number,
resourceId: ResourceID,
resourceType: ResourceType,
nodeName: string,
enabled = false
) {
return useQuery(
[
'environments',
environmentId,
'docker',
'images',
resourceType,
resourceId,
'status',
],
() =>
resourceType === ResourceType.SERVICE
? getServiceImagesStatus(environmentId, resourceId)
: getContainerImagesStatus(environmentId, resourceId, nodeName),
{
enabled,
}
);
}

@ -4,7 +4,7 @@ import UpdatesAvailable from '@/assets/ico/icon_updates-available.svg?c';
import UpToDate from '@/assets/ico/icon_up-to-date.svg?c';
import UpdatesUnknown from '@/assets/ico/icon_updates-unknown.svg?c';
import { ImageStatus } from '../../images/types';
import { ImageStatus } from './types';
export function statusIcon(status: ImageStatus) {
switch (status.Status) {

@ -0,0 +1,13 @@
type Status = 'outdated' | 'updated' | 'inprocess' | string;
export enum ResourceType {
CONTAINER,
SERVICE,
}
export interface ImageStatus {
Status: Status;
Message: string;
}
export type ResourceID = string;

@ -1,12 +1,40 @@
import { useQuery } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { ServiceId } from '@/react/docker/services/types';
import { ContainerId } from '@/react/docker/containers/types';
import { ContainerId } from '../containers/types';
import { ImageStatus, ResourceID, ResourceType } from './types';
import { ImageStatus } from './types';
export function useImageNotification(
environmentId: number,
resourceId: ResourceID,
resourceType: ResourceType,
nodeName: string,
enabled = false
) {
return useQuery(
[
'environments',
environmentId,
'docker',
'images',
resourceType,
resourceId,
'status',
],
() =>
resourceType === ResourceType.SERVICE
? getServiceImagesStatus(environmentId, resourceId)
: getContainerImagesStatus(environmentId, resourceId, nodeName),
{
enabled,
}
);
}
export async function getContainerImagesStatus(
async function getContainerImagesStatus(
environmentId: EnvironmentId,
containerID: ContainerId,
nodeName: string
@ -29,7 +57,7 @@ export async function getContainerImagesStatus(
}
}
export async function getServiceImagesStatus(
async function getServiceImagesStatus(
environmentId: EnvironmentId,
serviceID: ServiceId
) {

@ -10,8 +10,8 @@ import { Menu, MenuButton, MenuItem, MenuPopover } from '@reach/menu-button';
import { positionRight } from '@reach/popover';
import { useMemo } from 'react';
import { Environment } from '@/react/portainer/environments/types';
import { Authorized } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import {
@ -24,14 +24,12 @@ import { useTableState } from '@@/datatables/useTableState';
import { Button, ButtonGroup, LoadingButton } from '@@/buttons';
import { Link } from '@@/Link';
import { ButtonWithRef } from '@@/buttons/Button';
import { useRepeater } from '@@/datatables/useRepeater';
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { DockerImage } from '../../types';
import { ImagesListResponse, useImages } from '../../queries/useImages';
import { columns as defColumns } from './columns';
import { host as hostColumn } from './columns/host';
import { RowProvider } from './RowContext';
const tableKey = 'images';
@ -48,76 +46,67 @@ const settingsStore = createPersistedStore<TableSettings>(
);
export function ImagesDatatable({
dataset,
environment,
isHostColumnVisible,
isExportInProgress,
onDownload,
onRefresh,
onRemove,
}: {
dataset: Array<DockerImage>;
environment: Environment;
isHostColumnVisible: boolean;
onDownload: (images: Array<DockerImage>) => void;
onRemove: (images: Array<DockerImage>, force: true) => void;
onRefresh: () => Promise<void>;
onDownload: (images: Array<ImagesListResponse>) => void;
onRemove: (images: Array<ImagesListResponse>, force: true) => void;
isExportInProgress: boolean;
}) {
const environmentId = useEnvironmentId();
const tableState = useTableState(settingsStore, tableKey);
const columns = useMemo(
() => (isHostColumnVisible ? [...defColumns, hostColumn] : defColumns),
[isHostColumnVisible]
);
useRepeater(tableState.autoRefreshRate, onRefresh);
const imagesQuery = useImages(environmentId, true, {
refetchInterval: tableState.autoRefreshRate * 1000,
});
return (
<RowProvider context={{ environment }}>
<Datatable
title="Images"
titleIcon={List}
renderTableActions={(selectedItems) => (
<div className="flex items-center gap-2">
<RemoveButtonMenu
selectedItems={selectedItems}
onRemove={onRemove}
/>
<ImportExportButtons
isExportInProgress={isExportInProgress}
onExportClick={onDownload}
selectedItems={selectedItems}
/>
<Authorized authorizations="DockerImageBuild">
<Button
as={Link}
props={{ to: 'docker.images.build' }}
data-cy="image-buildImageButton"
icon={Plus}
>
Build a new image
</Button>
</Authorized>
</div>
)}
dataset={dataset}
settingsManager={tableState}
columns={columns}
emptyContentLabel="No images found"
renderTableSettings={() => (
<TableSettingsMenu>
<TableSettingsMenuAutoRefresh
value={tableState.autoRefreshRate}
onChange={(value) => tableState.setAutoRefreshRate(value)}
/>
</TableSettingsMenu>
)}
/>
</RowProvider>
<Datatable
title="Images"
titleIcon={List}
renderTableActions={(selectedItems) => (
<div className="flex items-center gap-2">
<RemoveButtonMenu selectedItems={selectedItems} onRemove={onRemove} />
<ImportExportButtons
isExportInProgress={isExportInProgress}
onExportClick={onDownload}
selectedItems={selectedItems}
/>
<Authorized authorizations="DockerImageBuild">
<Button
as={Link}
props={{ to: 'docker.images.build' }}
data-cy="image-buildImageButton"
icon={Plus}
>
Build a new image
</Button>
</Authorized>
</div>
)}
dataset={imagesQuery.data || []}
isLoading={imagesQuery.isLoading}
settingsManager={tableState}
columns={columns}
emptyContentLabel="No images found"
renderTableSettings={() => (
<TableSettingsMenu>
<TableSettingsMenuAutoRefresh
value={tableState.autoRefreshRate}
onChange={(value) => tableState.setAutoRefreshRate(value)}
/>
</TableSettingsMenu>
)}
/>
);
}
@ -125,8 +114,8 @@ function RemoveButtonMenu({
onRemove,
selectedItems,
}: {
selectedItems: Array<DockerImage>;
onRemove(selectedItems: Array<DockerImage>, force: boolean): void;
selectedItems: Array<ImagesListResponse>;
onRemove(selectedItems: Array<ImagesListResponse>, force: boolean): void;
}) {
return (
<Authorized authorizations="DockerImageDelete">
@ -176,8 +165,8 @@ function ImportExportButtons({
onExportClick,
}: {
isExportInProgress: boolean;
selectedItems: Array<DockerImage>;
onExportClick(selectedItems: Array<DockerImage>): void;
selectedItems: Array<ImagesListResponse>;
onExportClick(selectedItems: Array<ImagesListResponse>): void;
}) {
return (
<ButtonGroup>

@ -1,11 +0,0 @@
import { Environment } from '@/react/portainer/environments/types';
import { createRowContext } from '@@/datatables/RowContext';
interface RowContextState {
environment: Environment;
}
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
export { RowProvider, useRowContext };

@ -2,7 +2,7 @@ import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const created = columnHelper.accessor('Created', {
export const created = columnHelper.accessor('created', {
id: 'created',
header: 'Created',
cell: ({ getValue }) => {

@ -1,16 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { DockerImage } from '@/react/docker/images/types';
import { ImagesListResponse } from '@/react/docker/images/queries/useImages';
export const columnHelper = createColumnHelper<
DockerImage & { NodeName?: string }
>();
/**
* Docker response from proxy (with added portainer metadata)
* images view model
* images snapshot
* snapshots view model
*
*
*/
export const columnHelper = createColumnHelper<ImagesListResponse>();

@ -1,6 +1,6 @@
import { columnHelper } from './helper';
export const host = columnHelper.accessor('NodeName', {
export const host = columnHelper.accessor('nodeName', {
header: 'Host',
cell: ({ getValue }) => {
const value = getValue();

@ -1,21 +1,21 @@
import { CellContext, Column } from '@tanstack/react-table';
import { useSref } from '@uirouter/react';
import { DockerImage } from '@/react/docker/images/types';
import { truncate } from '@/portainer/filters/filters';
import { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
import { ImagesListResponse } from '@/react/docker/images/queries/useImages';
import { MultipleSelectionFilter } from '@@/datatables/Filter';
import { columnHelper } from './helper';
export const id = columnHelper.accessor('Id', {
export const id = columnHelper.accessor('id', {
id: 'id',
header: 'Id',
cell: Cell,
enableColumnFilter: true,
filterFn: (
{ original: { Used } },
{ original: { used } },
columnId,
filterValue: Array<'Used' | 'Unused'>
) => {
@ -23,11 +23,11 @@ export const id = columnHelper.accessor('Id', {
return true;
}
if (filterValue.includes('Used') && Used) {
if (filterValue.includes('Used') && used) {
return true;
}
if (filterValue.includes('Unused') && !Used) {
if (filterValue.includes('Unused') && !used) {
return true;
}
@ -63,12 +63,12 @@ function FilterByUsage<TData extends { Used: boolean }>({
function Cell({
getValue,
row: { original: image },
}: CellContext<DockerImage, string>) {
}: CellContext<ImagesListResponse, string>) {
const name = getValue();
const linkProps = useSref('.image', {
id: image.Id,
imageId: image.Id,
id: image.id,
imageId: image.id,
});
return (
@ -76,7 +76,7 @@ function Cell({
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
{truncate(name, 40)}
</a>
{!image.Used && (
{!image.used && (
<span className="label label-warning image-tag ml-2">Unused</span>
)}
</>

@ -2,7 +2,7 @@ import { humanize } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
export const size = columnHelper.accessor('VirtualSize', {
export const size = columnHelper.accessor('size', {
id: 'size',
header: 'Size',
cell: ({ getValue }) => {

@ -1,16 +1,16 @@
import { CellContext } from '@tanstack/react-table';
import { DockerImage } from '@/react/docker/images/types';
import { ImagesListResponse } from '@/react/docker/images/queries/useImages';
import { columnHelper } from './helper';
export const tags = columnHelper.accessor('RepoTags', {
export const tags = columnHelper.accessor('tags', {
id: 'tags',
header: 'Tags',
cell: Cell,
});
function Cell({ getValue }: CellContext<DockerImage, string[]>) {
function Cell({ getValue }: CellContext<ImagesListResponse, string[]>) {
const repoTags = getValue();
return (

@ -0,0 +1,6 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildUrl as buildDockerUrl } from '@/react/docker/queries/utils/build-url';
export function buildUrl(environmentId: EnvironmentId) {
return buildDockerUrl(environmentId, 'images');
}

@ -5,5 +5,6 @@ import { queryKeys as dockerQueryKeys } from '../../queries/utils';
export const queryKeys = {
base: (environmentId: EnvironmentId) =>
[dockerQueryKeys.root(environmentId), 'images'] as const,
list: (environmentId: EnvironmentId) => queryKeys.base(environmentId),
list: (environmentId: EnvironmentId, options: { withUsage?: boolean } = {}) =>
[...queryKeys.base(environmentId), options] as const,
};

@ -3,119 +3,51 @@ import { useQuery } from 'react-query';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildUrl } from '../../proxy/queries/build-url';
import { buildUrl } from './build-url';
import { queryKeys } from './queryKeys';
interface ImageSummary {
/**
* Number of containers using this image. Includes both stopped and running containers.
*
* This size is not calculated by default, and depends on which API endpoint is used.
* `-1` indicates that the value has not been set / calculated.
*
* Required: true
*/
Containers: number;
/**
* Date and time at which the image was created as a Unix timestamp
* (number of seconds sinds EPOCH).
*
* Required: true
*/
Created: number;
/**
* ID is the content-addressable ID of an image.
*
* This identifier is a content-addressable digest calculated from the
* image's configuration (which includes the digests of layers used by
* the image).
*
* Note that this digest differs from the `RepoDigests` below, which
* holds digests of image manifests that reference the image.
*
* Required: true
*/
Id: string;
/**
* User-defined key/value metadata.
* Required: true
*/
Labels: { [key: string]: string };
/**
* ID of the parent image.
*
* Depending on how the image was created, this field may be empty and
* is only set for images that were built/created locally. This field
* is empty if the image was pulled from an image registry.
*
* Required: true
*/
ParentId: string;
/**
* List of content-addressable digests of locally available image manifests
* that the image is referenced from. Multiple manifests can refer to the
* same image.
*
* These digests are usually only available if the image was either pulled
* from a registry, or if the image was pushed to a registry, which is when
* the manifest is generated and its digest calculated.
*
* Required: true
*/
RepoDigests: string[];
/**
* List of image names/tags in the local image cache that reference this
* image.
*
* Multiple image tags can refer to the same image, and this list may be
* empty if no tags reference the image, in which case the image is
* "untagged", in which case it can still be referenced by its ID.
*
* Required: true
*/
RepoTags: string[];
export interface ImagesListResponse {
created: number;
nodeName?: string;
id: string;
size: number;
tags: string[];
/**
* Total size of image layers that are shared between this image and other
* images.
*
* This size is not calculated by default. `-1` indicates that the value
* has not been set / calculated.
*
* Required: true
* Used is true if the image is used by at least one container.
* supplied only when withUsage is true
*/
SharedSize: number;
Size: number;
VirtualSize: number;
used: boolean;
}
type ImagesListResponse = ImageSummary[];
export function useImages<T = ImagesListResponse>(
export function useImages<T = Array<ImagesListResponse>>(
environmentId: EnvironmentId,
withUsage = false,
{
select,
enabled,
}: { select?(data: ImagesListResponse): T; enabled?: boolean } = {}
refetchInterval,
}: {
select?(data: Array<ImagesListResponse>): T;
enabled?: boolean;
refetchInterval?: number;
} = {}
) {
return useQuery(
queryKeys.list(environmentId),
() => getImages(environmentId),
{ select, enabled }
queryKeys.list(environmentId, { withUsage }),
() => getImages(environmentId, { withUsage }),
{ select, enabled, refetchInterval }
);
}
async function getImages(environmentId: EnvironmentId) {
async function getImages(
environmentId: EnvironmentId,
{ withUsage }: { withUsage?: boolean } = {}
) {
try {
const { data } = await axios.get<ImagesListResponse>(
buildUrl(environmentId, 'images', 'json')
const { data } = await axios.get<Array<ImagesListResponse>>(
buildUrl(environmentId),
{ params: { withUsage } }
);
return data;
} catch (err) {

@ -1,19 +1,5 @@
import { DockerImageResponse } from './types/response';
type Status = 'outdated' | 'updated' | 'inprocess' | string;
export enum ResourceType {
CONTAINER,
SERVICE,
}
export interface ImageStatus {
Status: Status;
Message: string;
}
export type ResourceID = string;
type DecoratedDockerImage = {
Used: boolean;
};

@ -0,0 +1,9 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
import { queryKeys as proxyQueryKeys } from '../query-keys';
export const queryKeys = {
base: (environmentId: EnvironmentId) =>
[proxyQueryKeys.base(environmentId), 'images'] as const,
list: (environmentId: EnvironmentId) => queryKeys.base(environmentId),
};

@ -0,0 +1,36 @@
import { useQuery } from 'react-query';
import { ImageSummary } from 'docker-types/generated/1.41';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { buildUrl } from '../build-url';
import { queryKeys } from './queryKeys';
type ImagesListResponse = Array<ImageSummary>;
export function useImages<T = ImagesListResponse>(
environmentId: EnvironmentId,
{
select,
enabled,
}: { select?(data: ImagesListResponse): T; enabled?: boolean } = {}
) {
return useQuery(
queryKeys.list(environmentId),
() => getImages(environmentId),
{ select, enabled }
);
}
async function getImages(environmentId: EnvironmentId) {
try {
const { data } = await axios.get<ImagesListResponse>(
buildUrl(environmentId, 'images', 'json')
);
return data;
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to retrieve images');
}
}

@ -0,0 +1,5 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
export function buildUrl(environmentId: EnvironmentId, path: string) {
return `/docker/${environmentId}/${path}`;
}

@ -1,5 +1,5 @@
import axios from '@/portainer/services/axios';
import { ImageStatus } from '@/react/docker/images/types';
import { ImageStatus } from '@/react/docker/components/ImageStatus/types';
export async function getStackImagesStatus(id: number) {
try {

Loading…
Cancel
Save