diff --git a/.eslintrc-typescript.yml b/.eslintrc-typescript.yml index b1a3d840..0e12fee5 100644 --- a/.eslintrc-typescript.yml +++ b/.eslintrc-typescript.yml @@ -5,9 +5,11 @@ extends: [ rules: "@typescript-eslint/ban-ts-comment": - error - - ts-expect-error: 'allow-with-description' - ts-ignore: 'allow-with-description' + - ts-expect-error: "allow-with-description" + ts-ignore: "allow-with-description" ts-nocheck: true ts-check: true minimumDescriptionLength: 5 "@typescript-eslint/ban-types": off + "@typescript-eslint/no-unused-vars": + - warn diff --git a/.eslintrc.yml b/.eslintrc.yml index 1e5a0553..8285c695 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -36,7 +36,7 @@ rules: prefer-const: - error - destructuring: any - no-console: 2 + no-console: 1 eqeqeq: error array-callback-return: warn # Temporarily disabled diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..258449ff --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "tabWidth": 2, + "singleQuote": false, + "jsxSingleQuote": false, + "bracketSpacing": true, + "tsxSingleQuote": true, + "tsSingleQuote": true +} \ No newline at end of file diff --git a/api/config/contentSources.ts b/api/config/contentSources.ts index 2047ca1f..2865b338 100644 --- a/api/config/contentSources.ts +++ b/api/config/contentSources.ts @@ -12,6 +12,8 @@ const config: ConfigFile = { 'listRepositories', 'listRepositoriesRpms', 'searchRpm', + 'listFeatures', + 'listSnapshotsByDate', ], }; diff --git a/api/schema/contentSources.json b/api/schema/contentSources.json index 1dc19fb5..90ed4f94 100644 --- a/api/schema/contentSources.json +++ b/api/schema/contentSources.json @@ -19,7 +19,56 @@ "type": "array" }, "uuids": { - "description": "List of RepositoryConfig UUIDs to search", + "description": "List of repository UUIDs to search", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "api.DetectRpmsRequest": { + "properties": { + "limit": { + "description": "Maximum number of records to return for the search", + "type": "integer" + }, + "rpm_names": { + "description": "List of rpm names to search", + "items": { + "type": "string" + }, + "type": "array" + }, + "urls": { + "description": "URLs of repositories to search", + "items": { + "type": "string" + }, + "type": "array" + }, + "uuids": { + "description": "List of repository UUIDs to search", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "api.DetectRpmsResponse": { + "properties": { + "found": { + "description": "List of rpm names found in given repositories", + "items": { + "type": "string" + }, + "type": "array" + }, + "missing": { + "description": "List of rpm names not found in given repositories", "items": { "type": "string" }, @@ -101,7 +150,7 @@ "type": "string" }, "repository_uuids": { - "description": "Repository uuids to find snapshots for", + "description": "Repository UUIDs to find snapshots for", "items": { "type": "string" }, @@ -112,16 +161,12 @@ }, "api.ListSnapshotByDateResponse": { "properties": { - "is_after": { - "description": "Is the snapshot after the specified date", - "type": "boolean" - }, - "match": { - "$ref": "#/components/schemas/api.SnapshotResponse" - }, - "repository_uuid": { - "description": "Repository uuid for associated snapshot", - "type": "string" + "data": { + "description": "Requested Data", + "items": { + "$ref": "#/components/schemas/api.SnapshotForDate" + }, + "type": "array" } }, "type": "object" @@ -211,6 +256,10 @@ "description": "Error of last attempted introspection", "type": "string" }, + "last_introspection_status": { + "description": "Status of last introspection", + "type": "string" + }, "last_introspection_time": { "description": "Timestamp of last attempted introspection", "type": "string" @@ -228,16 +277,12 @@ "type": "integer" }, "status": { - "description": "Introspection status of the repository", + "description": "Combined introspection and snapshot status of the repository", "type": "string" }, "url": { "description": "URL of the remote yum repository", "type": "string" - }, - "uuid": { - "description": "UUID of the repository if it exists for the user", - "type": "string" } }, "type": "object" @@ -453,10 +498,18 @@ "description": "GPG key for repository", "type": "string" }, + "label": { + "description": "Label used to configure the yum repository on clients", + "type": "string" + }, "last_introspection_error": { "description": "Error of last attempted introspection", "type": "string" }, + "last_introspection_status": { + "description": "Status of last introspection", + "type": "string" + }, "last_introspection_time": { "description": "Timestamp of last attempted introspection", "type": "string" @@ -464,6 +517,9 @@ "last_snapshot": { "$ref": "#/components/schemas/api.SnapshotResponse" }, + "last_snapshot_task": { + "$ref": "#/components/schemas/api.TaskInfoResponse" + }, "last_snapshot_task_uuid": { "description": "UUID of the last snapshot task", "type": "string" @@ -510,7 +566,7 @@ "type": "boolean" }, "status": { - "description": "Status of repository introspection (Valid, Invalid, Unavailable, Pending)", + "description": "Combined status of last introspection and snapshot of repository (Valid, Invalid, Unavailable, Pending)", "type": "string" }, "url": { @@ -528,7 +584,7 @@ "api.RepositoryRpm": { "properties": { "arch": { - "description": "The Architecture of the rpm", + "description": "The architecture of the rpm", "type": "string" }, "checksum": { @@ -645,6 +701,10 @@ "environment_name": { "description": "Environment found", "type": "string" + }, + "id": { + "description": "ID of the environment found", + "type": "string" } }, "type": "object" @@ -655,8 +715,12 @@ "description": "Description of the package group found", "type": "string" }, + "id": { + "description": "Package group ID", + "type": "string" + }, "package_group_name": { - "description": "Package group found", + "description": "Name of package group found", "type": "string" }, "package_list": { @@ -700,6 +764,22 @@ }, "type": "object" }, + "api.SnapshotForDate": { + "properties": { + "is_after": { + "description": "Is the snapshot after the specified date", + "type": "boolean" + }, + "match": { + "$ref": "#/components/schemas/api.SnapshotResponse" + }, + "repository_uuid": { + "description": "Repository uuid for associated snapshot", + "type": "string" + } + }, + "type": "object" + }, "api.SnapshotResponse": { "properties": { "added_counts": { @@ -741,6 +821,53 @@ }, "type": "object" }, + "api.SnapshotRpm": { + "properties": { + "arch": { + "description": "The architecture of the rpm", + "type": "string" + }, + "epoch": { + "description": "The epoch of the rpm", + "type": "string" + }, + "name": { + "description": "The rpm package name", + "type": "string" + }, + "release": { + "description": "The release of the rpm", + "type": "string" + }, + "summary": { + "description": "The summary of the rpm", + "type": "string" + }, + "version": { + "description": "The version of the rpm", + "type": "string" + } + }, + "type": "object" + }, + "api.SnapshotRpmCollectionResponse": { + "properties": { + "data": { + "description": "List of rpms", + "items": { + "$ref": "#/components/schemas/api.SnapshotRpm" + }, + "type": "array" + }, + "links": { + "$ref": "#/components/schemas/api.Links" + }, + "meta": { + "$ref": "#/components/schemas/api.ResponseMetadata" + } + }, + "type": "object" + }, "api.SnapshotSearchRpmRequest": { "properties": { "limit": { @@ -892,6 +1019,13 @@ "description": "Organization ID of the owner", "type": "string" }, + "repository_uuids": { + "description": "Repositories added to the template", + "items": { + "type": "string" + }, + "type": "array" + }, "uuid": { "readOnly": true, "type": "string" @@ -903,6 +1037,26 @@ }, "type": "object" }, + "api.TemplateUpdateRequest": { + "properties": { + "date": { + "description": "Latest date to include snapshots for", + "type": "string" + }, + "description": { + "description": "Description of the template", + "type": "string" + }, + "repository_uuids": { + "description": "Repositories to add to the template", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, "api.UUIDListRequest": { "properties": { "uuids": { @@ -1462,7 +1616,7 @@ } }, { - "description": "A comma separated list of uuids to control api response.", + "description": "A comma separated list of UUIDs to control api response.", "in": "query", "name": "uuid", "schema": { @@ -1478,7 +1632,7 @@ } }, { - "description": "A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`.", + "description": "A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`, `unavailable`.", "in": "query", "name": "status", "schema": { @@ -1825,83 +1979,6 @@ ] } }, - "/repositories/snapshots/for_date/": { - "post": { - "description": "Get nearest snapshot by date for a list of repositories.", - "operationId": "listSnapshotsByDate", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/api.ListSnapshotByDateRequest" - } - } - }, - "description": "request body", - "required": true, - "x-originalParamName": "body" - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/api.ListSnapshotByDateResponse" - }, - "type": "array" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Unauthorized" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "summary": "Get nearest snapshot by date for a list of repositories.", - "tags": [ - "snapshots" - ] - } - }, "/repositories/{uuid}": { "delete": { "description": "This enables deleting a specific repository.", @@ -2739,87 +2816,6 @@ ] } }, - "/repositories/{uuid}/snapshots/{snapshot_uuid}/config.repo": { - "get": { - "operationId": "getRepoConfigurationFile", - "parameters": [ - { - "description": "Identifier of the repository", - "in": "path", - "name": "uuid", - "required": true, - "schema": { - "type": "string" - } - }, - { - "description": "Identifier of the snapshot", - "in": "path", - "name": "snapshot_uuid", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - }, - "description": "OK" - }, - "400": { - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Bad Request" - }, - "401": { - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Unauthorized" - }, - "404": { - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Not Found" - }, - "500": { - "content": { - "text/plain": { - "schema": { - "$ref": "#/components/schemas/errors.ErrorResponse" - } - } - }, - "description": "Internal Server Error" - } - }, - "summary": "Get configuration file of a repository", - "tags": [ - "repositories" - ] - } - }, "/repository_gpg_key/{uuid}": { "get": { "description": "Get the GPG key file for a repository.", @@ -3195,6 +3191,341 @@ ] } }, + "/rpms/presence": { + "post": { + "description": "This enables users to detect presence of RPMs (Red Hat Package Manager) in a given list of repositories.", + "operationId": "detectRpm", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.DetectRpmsRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.DetectRpmsResponse" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Detect RPMs presence", + "tags": [ + "repositories", + "rpms" + ] + } + }, + "/snapshots/environments/names": { + "post": { + "description": "This enables users to search for environments in a given list of snapshots.", + "operationId": "searchSnapshotEnvironments", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.SnapshotSearchRpmRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/api.SearchEnvironmentResponse" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Search environments within snapshots", + "tags": [ + "snapshots", + "environments" + ] + } + }, + "/snapshots/for_date/": { + "post": { + "description": "Get nearest snapshot by date for a list of repositories.", + "operationId": "listSnapshotsByDate", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.ListSnapshotByDateRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.ListSnapshotByDateResponse" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get nearest snapshot by date for a list of repositories.", + "tags": [ + "snapshots" + ] + } + }, + "/snapshots/package_groups/names": { + "post": { + "description": "This enables users to search for package groups in a given list of snapshots.", + "operationId": "searchSnapshotPackageGroups", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.SnapshotSearchRpmRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/api.SearchPackageGroupResponse" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Search package groups within snapshots", + "tags": [ + "snapshots", + "environments" + ] + } + }, "/snapshots/rpms/names": { "post": { "description": "This enables users to search for RPMs (Red Hat Package Manager) in a given list of snapshots.", @@ -3283,6 +3614,175 @@ ] } }, + "/snapshots/{snapshot_uuid}/config.repo": { + "get": { + "operationId": "getRepoConfigurationFile", + "parameters": [ + { + "description": "Identifier of the snapshot", + "in": "path", + "name": "snapshot_uuid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get configuration file of a repository", + "tags": [ + "repositories" + ] + } + }, + "/snapshots/{uuid}/rpms": { + "get": { + "description": "List RPMs in a repository snapshot.", + "operationId": "listSnapshotRpms", + "parameters": [ + { + "description": "Snapshot ID.", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Number of items to include in response. Use it to control the number of items, particularly when dealing with large datasets. Default value: `100`.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "description": "Starting point for retrieving a subset of results. Determines how many items to skip from the beginning of the result set. Default value:`0`.", + "in": "query", + "name": "offset", + "schema": { + "type": "integer" + } + }, + { + "description": "Term to filter and retrieve items that match the specified search criteria. Search term can include name.", + "in": "query", + "name": "search", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.SnapshotRpmCollectionResponse" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "List Snapshot RPMs", + "tags": [ + "snapshots" + ] + } + }, "/tasks/": { "get": { "description": "Get the list of tasks.", @@ -3663,6 +4163,70 @@ } }, "/templates/{uuid}": { + "delete": { + "description": "This enables deleting a specific template.", + "operationId": "deleteTemplate", + "parameters": [ + { + "description": "Template ID.", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Template was successfully deleted" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Delete a template", + "tags": [ + "templates" + ] + }, "get": { "description": "Get template information.", "operationId": "getTemplate", @@ -3733,6 +4297,208 @@ "tags": [ "templates" ] + }, + "patch": { + "description": "This operation enables updating some subset of attributes of a template", + "operationId": "partialUpdateTemplate", + "parameters": [ + { + "description": "Template ID.", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.TemplateUpdateRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.TemplateResponse" + } + } + }, + "description": "Created", + "headers": { + "Location": { + "description": "resource URL", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Update some attributes of a Template", + "tags": [ + "templates" + ] + }, + "put": { + "description": "This operation enables updating all attributes of a template", + "operationId": "fullUpdateTemplate", + "parameters": [ + { + "description": "Template ID.", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.TemplateUpdateRequest" + } + } + }, + "description": "request body", + "required": true, + "x-originalParamName": "body" + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api.TemplateResponse" + } + } + }, + "description": "Created", + "headers": { + "Location": { + "description": "resource URL", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "415": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unsupported Media Type" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Fully update all attributes of a Template", + "tags": [ + "templates" + ] } } }, diff --git a/src/Components/CreateImageWizardV2/CreateImageWizard.scss b/src/Components/CreateImageWizardV2/CreateImageWizard.scss index 6aba0e42..4e86b6f9 100644 --- a/src/Components/CreateImageWizardV2/CreateImageWizard.scss +++ b/src/Components/CreateImageWizardV2/CreateImageWizard.scss @@ -72,3 +72,13 @@ ul.pf-m-plain { .panel-border { --pf-v5-c-panel--before--BorderColor: #BEE1F4; } + +// Targets the alert within the Reviewsteps > content dropdown +// Removes excess top margin padding +div.pf-v5-c-alert.pf-m-inline.pf-m-plain.pf-m-warning { + margin-top: 18px; + + h4 { + margin-block-start: 0; + } +} diff --git a/src/Components/CreateImageWizardV2/CreateImageWizard.tsx b/src/Components/CreateImageWizardV2/CreateImageWizard.tsx index 39183510..457f48e6 100644 --- a/src/Components/CreateImageWizardV2/CreateImageWizard.tsx +++ b/src/Components/CreateImageWizardV2/CreateImageWizard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Button, @@ -20,6 +20,7 @@ import RegistrationStep from './steps/Registration'; import RepositoriesStep from './steps/Repositories'; import ReviewStep from './steps/Review'; import ReviewWizardFooter from './steps/Review/Footer/Footer'; +import SnapshotStep from './steps/Snapshot'; import Aws from './steps/TargetEnvironment/Aws'; import Azure from './steps/TargetEnvironment/Azure'; import Gcp from './steps/TargetEnvironment/Gcp'; @@ -32,6 +33,7 @@ import { } from './validators'; import { RHEL_8, AARCH64 } from '../../constants'; +import { useListFeaturesQuery } from '../../store/contentSourcesApi'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import './CreateImageWizard.scss'; import { @@ -53,6 +55,8 @@ import { selectRegistrationType, selectStepValidation, addImageType, + selectSnapshotDate, + selectUseLatest, } from '../../store/wizardSlice'; import { resolveRelPath } from '../../Utilities/path'; import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader'; @@ -93,14 +97,33 @@ export const CustomWizardFooter = ({ }; type CreateImageWizardProps = { - startStepIndex?: number; + isEdit?: boolean; }; -const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => { +const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const [searchParams] = useSearchParams(); + // Remove this and all fallthrough logic when snapshotting is enabled in Prod-stable + // =========================TO REMOVE======================= + const { data, isSuccess, isFetching, isError } = + useListFeaturesQuery(undefined); + + const snapshottingEnabled = useMemo( + () => + !( + !isError && + !isFetching && + isSuccess && + data?.snapshots?.accessible === false && + data?.snapshots?.enabled === false + ), + [data, isSuccess, isFetching, isError] + ); + + // =========================TO REMOVE======================= + // IMPORTANT: Ensure the wizard starts with a fresh initial state useEffect(() => { dispatch(initializeWizard()); @@ -139,6 +162,11 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => { const registrationType = useAppSelector(selectRegistrationType); const activationKey = useAppSelector(selectActivationKey); + const snapshotDate = useAppSelector(selectSnapshotDate); + const useLatest = useAppSelector(selectUseLatest); + + const snapshotStepRequiresChoice = !useLatest && !snapshotDate; + const [currentStep, setCurrentStep] = React.useState(); const onStepChange = ( _event: React.MouseEvent, @@ -152,7 +180,7 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
navigate(resolveRelPath(''))} onStepChange={onStepChange} isVisitRequired @@ -268,10 +296,27 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => { name="Content" id="step-content" steps={[ + ...(snapshottingEnabled + ? [ + + } + > + + , + ] + : []), } > @@ -280,12 +325,13 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => { name="Additional packages" id="wizard-additional-packages" key="wizard-additional-packages" + isDisabled={snapshotStepRequiresChoice} footer={} > , ]} - > + /> { ? 'error' : 'default' } + isDisabled={snapshotStepRequiresChoice} footer={ { } > - + {/* Intentional prop drilling for simplicity - To be removed */} +
diff --git a/src/Components/CreateImageWizardV2/EditImageWizard.tsx b/src/Components/CreateImageWizardV2/EditImageWizard.tsx index ecc202d7..7a050b1b 100644 --- a/src/Components/CreateImageWizardV2/EditImageWizard.tsx +++ b/src/Components/CreateImageWizardV2/EditImageWizard.tsx @@ -36,7 +36,7 @@ const EditImageWizard = ({ blueprintId }: EditImageWizardProps) => { navigate(resolveRelPath('')); } }, [error, navigate]); - return ; + return ; }; export default EditImageWizard; diff --git a/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx b/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx index ace8c62b..3d30f820 100644 --- a/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx +++ b/src/Components/CreateImageWizardV2/steps/Repositories/Repositories.tsx @@ -351,7 +351,11 @@ const Repositories = () => { const handleSelectAll = () => { if (data) { - updateSelected(data.data?.map((repo) => repo.url) || []); + updateSelected( + data.data + ?.filter(({ status }) => status === 'Valid') + .map((repo) => repo.url) || [] + ); } }; diff --git a/src/Components/CreateImageWizardV2/steps/Review/ReviewStep.tsx b/src/Components/CreateImageWizardV2/steps/Review/ReviewStep.tsx index a32d9633..4c6839da 100644 --- a/src/Components/CreateImageWizardV2/steps/Review/ReviewStep.tsx +++ b/src/Components/CreateImageWizardV2/steps/Review/ReviewStep.tsx @@ -33,7 +33,7 @@ import { selectRegistrationType, } from '../../../../store/wizardSlice'; -const Review = () => { +const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => { const blueprintName = useAppSelector(selectBlueprintName); const blueprintDescription = useAppSelector(selectBlueprintDescription); const distribution = useAppSelector(selectDistribution); @@ -172,7 +172,8 @@ const Review = () => { isIndented data-testid="content-expandable" > - + {/* Intentional prop drilling for simplicity - To be removed */} + {(blueprintName || blueprintDescription) && ( { ); }; +const Error = () => { + return ( + + Repositories cannot be reached, try again later. + + ); +}; + +const Loading = () => { + return ( + + } + headingLevel="h4" + /> + + ); +}; + +export const SnapshotTable = ({ + snapshotForDate, +}: { + snapshotForDate: ApiSnapshotForDate[]; +}) => { + const { data, isSuccess, isLoading, isError } = useListRepositoriesQuery({ + uuid: snapshotForDate.map(({ repository_uuid }) => repository_uuid).join(), + origin: 'red_hat,external', // Make sure to show both redhat and custom + }); + + const isAfterSet = new Set( + snapshotForDate + .filter(({ is_after }) => is_after) + .map(({ repository_uuid }) => repository_uuid) + ); + + const stringToDateToMMDDYYYY = (strDate: string) => { + const date = new Date(strDate); + return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date + .getDate() + .toString() + .padStart(2, '0')}/${date.getFullYear()}`; + }; + + return ( + (isError && ) || + (isLoading && ) || + (isSuccess && ( + + + + + + + + + + + {data?.data?.map(({ uuid, name, last_snapshot }, pkgIndex) => ( + + + + + ))} + +
NameLast snapshot date
{name} + {uuid && isAfterSet.has(uuid) ? ( + + ) : last_snapshot?.created_at ? ( + stringToDateToMMDDYYYY(last_snapshot.created_at) + ) : ( + 'N/A' + )} +
+
+
+ )) + ); +}; + export const PackagesTable = () => { const packages = useAppSelector(selectPackages); return ( diff --git a/src/Components/CreateImageWizardV2/steps/Review/ReviewStepTextLists.tsx b/src/Components/CreateImageWizardV2/steps/Review/ReviewStepTextLists.tsx index 58ef6f13..a8bf7cbb 100644 --- a/src/Components/CreateImageWizardV2/steps/Review/ReviewStepTextLists.tsx +++ b/src/Components/CreateImageWizardV2/steps/Review/ReviewStepTextLists.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Alert, @@ -16,7 +16,11 @@ import { import { ExclamationTriangleIcon } from '@patternfly/react-icons'; import ActivationKeyInformation from './../Registration/ActivationKeyInformation'; -import { PackagesTable, RepositoriesTable } from './ReviewStepTables'; +import { + PackagesTable, + RepositoriesTable, + SnapshotTable, +} from './ReviewStepTables'; import { FSReviewTable } from './ReviewStepTables'; import { @@ -26,6 +30,7 @@ import { RHEL_8_MAINTENANCE_SUPPORT, RHEL_9, } from '../../../../constants'; +import { useListSnapshotsByDateMutation } from '../../../../store/contentSourcesApi'; import { useAppSelector } from '../../../../store/hooks'; import { useGetSourceListQuery } from '../../../../store/provisioningApi'; import { useShowActivationKeyQuery } from '../../../../store/rhsmApi'; @@ -51,8 +56,14 @@ import { selectRegistrationType, selectFileSystemPartitionMode, selectRecommendedRepositories, + selectSnapshotDate, + selectUseLatest, } from '../../../../store/wizardSlice'; -import { toMonthAndYear } from '../../../../Utilities/time'; +import { + convertMMDDYYYYToYYYYMMDD, + toMonthAndYear, + yyyyMMddFormat, +} from '../../../../Utilities/time'; import { MinimumSizePopover } from '../FileSystem/FileSystemTable'; import { MajorReleasesLifecyclesChart } from '../ImageOutput/ReleaseLifecycle'; import OscapProfileInformation from '../Oscap/OscapProfileInformation'; @@ -385,17 +396,126 @@ export const TargetEnvOtherList = () => { ); }; -export const ContentList = () => { +export const ContentList = ({ + snapshottingEnabled, +}: { + snapshottingEnabled: boolean; +}) => { const customRepositories = useAppSelector(selectCustomRepositories); const packages = useAppSelector(selectPackages); const recommendedRepositories = useAppSelector(selectRecommendedRepositories); + const snapshotDate = useAppSelector(selectSnapshotDate); + const useLatest = useAppSelector(selectUseLatest); + + const customAndRecommendedRepositoryUUIDS = useMemo( + () => + [ + ...customRepositories.map(({ id }) => id), + ...recommendedRepositories.map(({ uuid }) => uuid), + ] as string[], + [customRepositories, recommendedRepositories] + ); + + const [listSnapshotsByDate, { data, isSuccess, isLoading, isError }] = + useListSnapshotsByDateMutation(); + + useEffect(() => { + listSnapshotsByDate({ + apiListSnapshotByDateRequest: { + repository_uuids: customAndRecommendedRepositoryUUIDS, + date: useLatest + ? yyyyMMddFormat(new Date()) + : convertMMDDYYYYToYYYYMMDD(snapshotDate), + }, + }); + }, [ + customAndRecommendedRepositoryUUIDS, + listSnapshotsByDate, + snapshotDate, + useLatest, + ]); + const duplicatePackages = packages.filter( (item, index) => packages.indexOf(item) !== index ); + + const noRepositoriesSelected = + customAndRecommendedRepositoryUUIDS.length === 0; + + const hasSnapshotDateAfter = data?.data?.some(({ is_after }) => is_after); + + const snapshottingText = useMemo(() => { + switch (true) { + case noRepositoriesSelected: + return 'No repositories selected'; + case isLoading: + return ''; + case useLatest: + return 'Use latest'; + case !!snapshotDate: + return `State as of ${snapshotDate}`; + default: + return ''; + } + }, [noRepositoriesSelected, isLoading, useLatest, snapshotDate]); + return ( <> + {snapshottingEnabled ? ( + <> + + Repository Snapshot + + + + } + > + + + {!useLatest && + !isLoading && + isSuccess && + hasSnapshotDateAfter ? ( + + ) : ( + '' + )} + + + ) : ( + '' + )} Custom repositories diff --git a/src/Components/CreateImageWizardV2/steps/Review/index.tsx b/src/Components/CreateImageWizardV2/steps/Review/index.tsx index 4747d42e..56b977a0 100644 --- a/src/Components/CreateImageWizardV2/steps/Review/index.tsx +++ b/src/Components/CreateImageWizardV2/steps/Review/index.tsx @@ -4,13 +4,18 @@ import { Form, Title } from '@patternfly/react-core'; import Review from './ReviewStep'; -const ReviewStep = () => { +const ReviewStep = ({ + snapshottingEnabled, +}: { + snapshottingEnabled: boolean; +}) => { return (
Review - + {/* Intentional prop drilling for simplicity - To be removed */} + ); }; diff --git a/src/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.tsx b/src/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.tsx new file mode 100644 index 00000000..5eebe8a8 --- /dev/null +++ b/src/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.tsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import { + Alert, + Button, + DatePicker, + Flex, + FormGroup, + Grid, + Radio, + Text, + Title, +} from '@patternfly/react-core'; + +import ConditionalTooltip from './components/ConditionalTooltip'; + +import { useListFeaturesQuery } from '../../../../store/contentSourcesApi'; +import { useAppDispatch, useAppSelector } from '../../../../store/hooks'; +import { + selectSnapshotDate, + selectUseLatest, + changeUseLatest, + changeSnapshotDate, +} from '../../../../store/wizardSlice'; +import { + dateToMMDDYYYY, + parseMMDDYYYYtoDate, +} from '../../../../Utilities/time'; + +const dateValidators = [ + (date: Date) => { + if (date.getTime() > Date.now()) { + return 'Cannot set a date in the future'; + } + return ''; + }, +]; + +export default function Snapshot() { + const dispatch = useAppDispatch(); + const snapshotDate = useAppSelector(selectSnapshotDate); + const useLatest = useAppSelector(selectUseLatest); + return ( + <> + + !useLatest && dispatch(changeUseLatest(true))} + /> + useLatest && dispatch(changeUseLatest(false))} + /> + + {useLatest ? ( + <> + + Use latest content + + + + Image Builder will automatically use the newest state of + repositories when building this image. + + + + ) : ( + <> + + Use a snapshot + + + + dispatch(changeSnapshotDate(val))} + /> + + + + + + Image Builder will reflect the state of repositories based on the + selected date when building this image. + + + + )} + + ); +} diff --git a/src/Components/CreateImageWizardV2/steps/Snapshot/components/ConditionalTooltip.tsx b/src/Components/CreateImageWizardV2/steps/Snapshot/components/ConditionalTooltip.tsx new file mode 100644 index 00000000..72c7c551 --- /dev/null +++ b/src/Components/CreateImageWizardV2/steps/Snapshot/components/ConditionalTooltip.tsx @@ -0,0 +1,25 @@ +import React, { cloneElement } from 'react'; + +import { Tooltip, TooltipProps } from '@patternfly/react-core'; + +interface Props extends TooltipProps { + show: boolean; + setDisabled?: boolean; +} + +const ConditionalTooltip = ({ show, children, setDisabled, ...rest }: Props) => + show ? ( + +
+ {children && + cloneElement( + children, + setDisabled ? { isDisabled: setDisabled } : undefined + )} +
+
+ ) : ( +
{children}
+ ); + +export default ConditionalTooltip; diff --git a/src/Components/CreateImageWizardV2/steps/Snapshot/index.tsx b/src/Components/CreateImageWizardV2/steps/Snapshot/index.tsx new file mode 100644 index 00000000..928cad5b --- /dev/null +++ b/src/Components/CreateImageWizardV2/steps/Snapshot/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { Button, Form, Grid, Text, Title } from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import { useHref } from 'react-router-dom'; + +import Snapshot from './Snapshot'; + +export default function SnapshotStep() { + const path = useHref('image-builder'); + const pathname = path.split('image-builder')[0] + 'content'; + return ( +
+ + Repository snapshot + + + + Control the consistency of the packages in the repository used to + build the image. + + + + + + ); +} diff --git a/src/Components/CreateImageWizardV2/utilities/requestMapper.tsx b/src/Components/CreateImageWizardV2/utilities/requestMapper.tsx index 1b81a436..b4cc5bdf 100644 --- a/src/Components/CreateImageWizardV2/utilities/requestMapper.tsx +++ b/src/Components/CreateImageWizardV2/utilities/requestMapper.tsx @@ -46,7 +46,13 @@ import { wizardState, selectFileSystemPartitionMode, selectPartitions, + selectSnapshotDate, + selectUseLatest, } from '../../../store/wizardSlice'; +import { + convertMMDDYYYYToYYYYMMDD, + convertYYYYMMDDTOMMDDYYYY, +} from '../../../Utilities/time'; import { convertSchemaToIBCustomRepo, convertSchemaToIBPayloadRepo, @@ -102,6 +108,11 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => { (image) => image.image_type === 'azure' ); + const snapshot_date = convertYYYYMMDDTOMMDDYYYY( + request.image_requests.find((image) => !!image.snapshot_date) + ?.snapshot_date || '' + ); + const awsUploadOptions = aws?.upload_request .options as AwsUploadRequestOptions; const gcpUploadOptions = gcp?.upload_request @@ -154,6 +165,10 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => { source: { id: awsUploadOptions?.share_with_sources?.[0] }, sourceId: awsUploadOptions?.share_with_sources?.[0], }, + snapshotting: { + useLatest: !snapshot_date, + snapshotDate: snapshot_date, + }, repositories: { customRepositories: request.customizations.custom_repositories || [], payloadRepositories: request.customizations.payload_repositories || [], @@ -179,6 +194,8 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => { const getImageRequests = (state: RootState): ImageRequest[] => { const imageTypes = selectImageTypes(state); + const snapshotDate = convertMMDDYYYYToYYYYMMDD(selectSnapshotDate(state)); + const useLatest = selectUseLatest(state); return imageTypes.map((type) => ({ architecture: selectArchitecture(state), image_type: type, @@ -186,6 +203,7 @@ const getImageRequests = (state: RootState): ImageRequest[] => { type: uploadTypeByTargetEnv(type), options: getImageOptions(type, state), }, + snapshot_date: useLatest ? '' : snapshotDate, })); }; diff --git a/src/Utilities/time.js b/src/Utilities/time.js deleted file mode 100644 index a9a31113..00000000 --- a/src/Utilities/time.js +++ /dev/null @@ -1,49 +0,0 @@ -export const timestampToDisplayString = (ts) => { - // timestamp has format 2021-04-27T12:31:12Z - // must be converted to ms timestamp and then reformatted to Apr 27, 2021 - if (!ts) { - return ''; - } - - // get YYYY-MM-DD format - const ms = Date.parse(ts); - const options = { month: 'short', day: 'numeric', year: 'numeric' }; - const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms); - return tsDisplay; -}; - -export const convertStringToDate = (createdAtAsString) => { - if (isNaN(Date.parse(createdAtAsString))) { - // converts property created_at of the image object from string to UTC - const [dateValues, timeValues] = createdAtAsString.split(' '); - const datetimeString = `${dateValues}T${timeValues}Z`; - return Date.parse(datetimeString); - } else { - return Date.parse(createdAtAsString); - } -}; - -export const computeHoursToExpiration = (imageCreatedAt) => { - if (imageCreatedAt) { - const currentTime = Date.now(); - // miliseconds in hour - needed for calculating the difference - // between current date and the date of the image creation - const msInHour = 1000 * 60 * 60; - const timeUntilExpiration = Math.floor( - (currentTime - convertStringToDate(imageCreatedAt)) / msInHour - ); - return timeUntilExpiration; - } else { - // when creating a new image, the compose.created_at can be undefined when first queued - return 0; - } -}; - -export const toMonthAndYear = (dateString) => { - const options = { - year: 'numeric', - month: 'long', - }; - const date = new Date(dateString); - return date.toLocaleDateString('en-US', options); -}; diff --git a/src/Utilities/time.ts b/src/Utilities/time.ts new file mode 100644 index 00000000..becfae3e --- /dev/null +++ b/src/Utilities/time.ts @@ -0,0 +1,85 @@ +export const parseMMDDYYYYtoDate = (val: string) => { + const [mm, dd, yyyy] = val.split('/'); + const newVal = `${yyyy}-${mm}-${dd}`; + return mm && dd && yyyy ? new Date(`${newVal}T00:00:00`) : new Date(''); +}; + +export const parseYYYYMMDDToDate = (val: string) => + val ? new Date(`${val}T00:00:00`) : new Date(''); + +export const yyyyMMddFormat = (date: Date) => + `${date.getFullYear()}-${(date.getMonth() + 1) + .toString() + .padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; + +export const convertMMDDYYYYToYYYYMMDD = (dateStr: string) => { + if (!dateStr) return ''; + const date = parseMMDDYYYYtoDate(dateStr); + return yyyyMMddFormat(date); +}; + +export const dateToMMDDYYYY = (date: Date) => + `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date + .getDate() + .toString() + .padStart(2, '0')}/${date.getFullYear()}`; + +export const convertYYYYMMDDTOMMDDYYYY = (dateStr: string) => { + if (!dateStr) return ''; + const date = parseYYYYMMDDToDate(dateStr); + return dateToMMDDYYYY(date); +}; + +export const toMonthAndYear = (dateString: string) => { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + }; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', options); +}; + +export const timestampToDisplayString = (ts?: string) => { + // timestamp has format 2021-04-27T12:31:12Z + // must be converted to ms timestamp and then reformatted to Apr 27, 2021 + if (!ts) { + return ''; + } + + // get YYYY-MM-DD format + const ms = Date.parse(ts); + const options: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + }; + const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms); + return tsDisplay; +}; + +export const convertStringToDate = (createdAtAsString: string = '') => { + if (isNaN(Date.parse(createdAtAsString))) { + // converts property created_at of the image object from string to UTC + const [dateValues, timeValues] = createdAtAsString.split(' '); + const datetimeString = `${dateValues}T${timeValues}Z`; + return Date.parse(datetimeString); + } else { + return Date.parse(createdAtAsString); + } +}; + +export const computeHoursToExpiration = (imageCreatedAt: string) => { + if (imageCreatedAt) { + const currentTime = Date.now(); + // miliseconds in hour - needed for calculating the difference + // between current date and the date of the image creation + const msInHour = 1000 * 60 * 60; + const timeUntilExpiration = Math.floor( + (currentTime - convertStringToDate(imageCreatedAt)) / msInHour + ); + return timeUntilExpiration; + } else { + // when creating a new image, the compose.created_at can be undefined when first queued + return 0; + } +}; diff --git a/src/store/contentSourcesApi.ts b/src/store/contentSourcesApi.ts index 65086070..5d274017 100644 --- a/src/store/contentSourcesApi.ts +++ b/src/store/contentSourcesApi.ts @@ -1,6 +1,9 @@ import { emptyContentSourcesApi as api } from "./emptyContentSourcesApi"; const injectedRtkApi = api.injectEndpoints({ endpoints: (build) => ({ + listFeatures: build.query({ + query: () => ({ url: `/features/` }), + }), listRepositories: build.query< ListRepositoriesApiResponse, ListRepositoriesApiArg @@ -56,10 +59,22 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.apiContentUnitSearchRequest, }), }), + listSnapshotsByDate: build.mutation< + ListSnapshotsByDateApiResponse, + ListSnapshotsByDateApiArg + >({ + query: (queryArg) => ({ + url: `/snapshots/for_date/`, + method: "POST", + body: queryArg.apiListSnapshotByDateRequest, + }), + }), }), overrideExisting: false, }); export { injectedRtkApi as contentSourcesApi }; +export type ListFeaturesApiResponse = /** status 200 OK */ ApiFeatureSet; +export type ListFeaturesApiArg = void; export type ListRepositoriesApiResponse = /** status 200 OK */ ApiRepositoryCollectionResponseRead; export type ListRepositoriesApiArg = { @@ -81,11 +96,11 @@ export type ListRepositoriesApiArg = { name?: string; /** A comma separated list of URLs to control api response. */ url?: string; - /** A comma separated list of uuids to control api response. */ + /** A comma separated list of UUIDs to control api response. */ uuid?: string; /** Sort the response data based on specific repository parameters. Sort criteria can include `name`, `url`, `status`, and `package_count`. */ sortBy?: string; - /** A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`. */ + /** A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`, `unavailable`. */ status?: string; /** A comma separated list of origins to filter api response. Origins can include `red_hat` and `external`. */ origin?: string; @@ -117,6 +132,21 @@ export type SearchRpmApiArg = { /** request body */ apiContentUnitSearchRequest: ApiContentUnitSearchRequest; }; +export type ListSnapshotsByDateApiResponse = + /** status 200 OK */ ApiListSnapshotByDateResponse; +export type ListSnapshotsByDateApiArg = { + /** request body */ + apiListSnapshotByDateRequest: ApiListSnapshotByDateRequest; +}; +export type ApiFeature = { + /** Whether the current user can access the feature */ + accessible?: boolean; + /** Whether the feature is enabled on the running server */ + enabled?: boolean; +}; +export type ApiFeatureSet = { + [key: string]: ApiFeature; +}; export type ApiSnapshotResponse = { /** Count of each content type */ added_counts?: { @@ -138,6 +168,26 @@ export type ApiSnapshotResponse = { url?: string; uuid?: string; }; +export type ApiTaskInfoResponse = { + /** Timestamp of task creation */ + created_at?: string; + /** Timestamp task ended running at */ + ended_at?: string; + /** Error thrown while running task */ + error?: string; + /** Organization ID of the owner */ + org_id?: string; + /** Name of the associated repository */ + repository_name?: string; + /** UUID of the associated repository */ + repository_uuid?: string; + /** Status of task (running, failed, completed, canceled, pending) */ + status?: string; + /** Type of task */ + type?: string; + /** UUID of the object */ + uuid?: string; +}; export type ApiRepositoryResponse = { /** Content Type (rpm) of the repository */ content_type?: string; @@ -149,11 +199,16 @@ export type ApiRepositoryResponse = { failed_introspections_count?: number; /** GPG key for repository */ gpg_key?: string; + /** Label used to configure the yum repository on clients */ + label?: string; /** Error of last attempted introspection */ last_introspection_error?: string; + /** Status of last introspection */ + last_introspection_status?: string; /** Timestamp of last attempted introspection */ last_introspection_time?: string; last_snapshot?: ApiSnapshotResponse; + last_snapshot_task?: ApiTaskInfoResponse; /** UUID of the last snapshot task */ last_snapshot_task_uuid?: string; /** UUID of the last dao.Snapshot */ @@ -174,7 +229,7 @@ export type ApiRepositoryResponse = { package_count?: number; /** Enable snapshotting and hosting of this repository */ snapshot?: boolean; - /** Status of repository introspection (Valid, Invalid, Unavailable, Pending) */ + /** Combined status of last introspection and snapshot of repository (Valid, Invalid, Unavailable, Pending) */ status?: string; /** URL of the remote yum repository */ url?: string; @@ -192,11 +247,16 @@ export type ApiRepositoryResponseRead = { failed_introspections_count?: number; /** GPG key for repository */ gpg_key?: string; + /** Label used to configure the yum repository on clients */ + label?: string; /** Error of last attempted introspection */ last_introspection_error?: string; + /** Status of last introspection */ + last_introspection_status?: string; /** Timestamp of last attempted introspection */ last_introspection_time?: string; last_snapshot?: ApiSnapshotResponse; + last_snapshot_task?: ApiTaskInfoResponse; /** UUID of the last snapshot task */ last_snapshot_task_uuid?: string; /** UUID of the last dao.Snapshot */ @@ -219,7 +279,7 @@ export type ApiRepositoryResponseRead = { package_count?: number; /** Enable snapshotting and hosting of this repository */ snapshot?: boolean; - /** Status of repository introspection (Valid, Invalid, Unavailable, Pending) */ + /** Combined status of last introspection and snapshot of repository (Valid, Invalid, Unavailable, Pending) */ status?: string; /** URL of the remote yum repository */ url?: string; @@ -286,7 +346,7 @@ export type ApiRepositoryRequest = { url?: string; }; export type ApiRepositoryRpm = { - /** The Architecture of the rpm */ + /** The architecture of the rpm */ arch?: string; /** The checksum of the rpm */ checksum?: string; @@ -322,12 +382,31 @@ export type ApiContentUnitSearchRequest = { search?: string; /** URLs of repositories to search */ urls?: string[]; - /** List of RepositoryConfig UUIDs to search */ + /** List of repository UUIDs to search */ uuids?: string[]; }; +export type ApiSnapshotForDate = { + /** Is the snapshot after the specified date */ + is_after?: boolean; + match?: ApiSnapshotResponse; + /** Repository uuid for associated snapshot */ + repository_uuid?: string; +}; +export type ApiListSnapshotByDateResponse = { + /** Requested Data */ + data?: ApiSnapshotForDate[]; +}; +export type ApiListSnapshotByDateRequest = { + /** Exact date to search by. */ + date?: string; + /** Repository UUIDs to find snapshots for */ + repository_uuids?: string[]; +}; export const { + useListFeaturesQuery, useListRepositoriesQuery, useCreateRepositoryMutation, useListRepositoriesRpmsQuery, useSearchRpmMutation, + useListSnapshotsByDateMutation, } = injectedRtkApi; diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index e24b89d9..75e41ba1 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -77,6 +77,10 @@ export type wizardState = { partitions: Partition[]; isNextButtonTouched: boolean; }; + snapshotting: { + useLatest: boolean; + snapshotDate: string; + }; repositories: { customRepositories: CustomRepository[]; payloadRepositories: Repository[]; @@ -136,6 +140,10 @@ const initialState: wizardState = { partitions: [], isNextButtonTouched: true, }, + snapshotting: { + useLatest: true, + snapshotDate: '', + }, repositories: { customRepositories: [], payloadRepositories: [], @@ -241,6 +249,13 @@ export const selectPartitions = (state: RootState) => { return state.wizard.fileSystem.partitions; }; +export const selectUseLatest = (state: RootState) => { + return state.wizard.snapshotting.useLatest; +}; +export const selectSnapshotDate = (state: RootState) => { + return state.wizard.snapshotting.snapshotDate; +}; + export const selectCustomRepositories = (state: RootState) => { return state.wizard.repositories.customRepositories; }; @@ -505,6 +520,12 @@ export const wizardSlice = createSlice({ state.fileSystem.partitions[partitionIndex].min_size = min_size; } }, + changeUseLatest: (state, action: PayloadAction) => { + state.snapshotting.useLatest = action.payload; + }, + changeSnapshotDate: (state, action: PayloadAction) => { + state.snapshotting.snapshotDate = action.payload; + }, changeCustomRepositories: ( state, action: PayloadAction @@ -624,6 +645,8 @@ export const { changePartitionUnit, changePartitionMinSize, changePartitionOrder, + changeUseLatest, + changeSnapshotDate, changeCustomRepositories, changePayloadRepositories, addRecommendedRepository, diff --git a/src/test/Components/CreateImageWizardV2/CreateImageWizard.compliance.test.tsx b/src/test/Components/CreateImageWizardV2/CreateImageWizard.compliance.test.tsx index 8a080cdf..d11c8c1b 100644 --- a/src/test/Components/CreateImageWizardV2/CreateImageWizard.compliance.test.tsx +++ b/src/test/Components/CreateImageWizardV2/CreateImageWizard.compliance.test.tsx @@ -106,6 +106,7 @@ describe('Step Compliance', () => { }) ).not.toBeInTheDocument(); + await clickNext(); // skip RepositorySnapshot await clickNext(); // skip Repositories // check that there are no Packages contained when selecting the "None" profile option @@ -172,6 +173,7 @@ describe('Step Compliance', () => { await screen.findByRole('heading', { name: /File system configuration/i }); await screen.findByText(/tmp/i); + await clickNext(); // skip RepositorySnapshots await clickNext(); // skip Repositories // check that the Packages contains correct packages diff --git a/src/test/Components/CreateImageWizardV2/CreateImageWizard.content.test.tsx b/src/test/Components/CreateImageWizardV2/CreateImageWizard.content.test.tsx index a46a98e5..91d8d05e 100644 --- a/src/test/Components/CreateImageWizardV2/CreateImageWizard.content.test.tsx +++ b/src/test/Components/CreateImageWizardV2/CreateImageWizard.content.test.tsx @@ -155,6 +155,8 @@ describe('Step Packages', () => { await clickNext(); // skip OpenSCAP await clickNext(); + // skip snapshots + await clickNext(); // skip Repositories await clickNext(); // skip fsc @@ -431,6 +433,8 @@ describe('Step Custom repositories', () => { await clickNext(); // skip fsc await clickNext(); + // skip snapshots + await clickNext(); }; test('selected repositories stored in and retrieved from form state', async () => { diff --git a/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx b/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx index 6e48d184..d061c2eb 100644 --- a/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx +++ b/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx @@ -410,6 +410,7 @@ describe('Step Upload to AWS', () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); @@ -602,6 +603,7 @@ describe('Step Registration', () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); const review = await screen.findByTestId('review-registration'); @@ -648,6 +650,7 @@ describe('Step Registration', () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); const review = await screen.findByTestId('review-registration'); @@ -695,6 +698,7 @@ describe('Step Registration', () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); const review = await screen.findByTestId('review-registration'); @@ -725,6 +729,7 @@ describe('Step Registration', () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); await screen.findByText('Register the system later'); @@ -858,6 +863,8 @@ describe('Step Details', () => { await clickNext(); // skip fsc await clickNext(); + // skip snapshots + await clickNext(); }; test('image name invalid for more than 63 chars', async () => { @@ -932,6 +939,8 @@ describe('Step Review', () => { await clickNext(); // skip OpenScap await clickNext(); + // skip snpashotstep + await clickNext(); // skip repositories await clickNext(); // skip packages @@ -993,7 +1002,8 @@ describe('Step Review', () => { // skip Oscap await clickNext(); - + // skip snpashotstep + await clickNext(); // skip packages await clickNext(); // skip repositories diff --git a/src/test/Components/CreateImageWizardV2/steps/Details/Details.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Details/Details.test.tsx index 37bae1f8..6ff41cdf 100644 --- a/src/test/Components/CreateImageWizardV2/steps/Details/Details.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/Details/Details.test.tsx @@ -38,6 +38,7 @@ const goToDetailsStep = async () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); }; const enterBlueprintDescription = async () => { diff --git a/src/test/Components/CreateImageWizardV2/steps/FileSystemConfiguration/FileSystemConfiguration.test.tsx b/src/test/Components/CreateImageWizardV2/steps/FileSystemConfiguration/FileSystemConfiguration.test.tsx index cacbd8eb..294efb75 100644 --- a/src/test/Components/CreateImageWizardV2/steps/FileSystemConfiguration/FileSystemConfiguration.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/FileSystemConfiguration/FileSystemConfiguration.test.tsx @@ -97,6 +97,7 @@ const goToReviewStep = async () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); }; diff --git a/src/test/Components/CreateImageWizardV2/steps/ImageOutput/TargetEnvironment.test.tsx b/src/test/Components/CreateImageWizardV2/steps/ImageOutput/TargetEnvironment.test.tsx index a6011911..0d75acb1 100644 --- a/src/test/Components/CreateImageWizardV2/steps/ImageOutput/TargetEnvironment.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/ImageOutput/TargetEnvironment.test.tsx @@ -57,6 +57,7 @@ const clickToReview = async () => { await clickNext(); // skip Registration await clickNext(); // skip OSCAP await clickNext(); // skip FSC + await clickNext(); // skip SnapshotRepositories await clickNext(); // skip Repositories await clickNext(); // skip Packages const nameInput = await screen.findByRole('textbox', { diff --git a/src/test/Components/CreateImageWizardV2/steps/Oscap/Oscap.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Oscap/Oscap.test.tsx index f3d03891..60db208d 100644 --- a/src/test/Components/CreateImageWizardV2/steps/Oscap/Oscap.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/Oscap/Oscap.test.tsx @@ -81,6 +81,7 @@ const selectNone = async () => { const goToReviewStep = async () => { await clickNext(); // File system configuration + await clickNext(); // Snapshot repositories await clickNext(); // Custom repositories await clickNext(); // Additional packages await clickNext(); // Details diff --git a/src/test/Components/CreateImageWizardV2/steps/Packages/Packages.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Packages/Packages.test.tsx index 95657fe1..8727dc77 100644 --- a/src/test/Components/CreateImageWizardV2/steps/Packages/Packages.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/Packages/Packages.test.tsx @@ -47,6 +47,7 @@ const goToPackagesStep = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File System + await clickNext(); // Snapshots await clickNext(); // Custom repositories await clickNext(); // Additional packages }; diff --git a/src/test/Components/CreateImageWizardV2/steps/Registration/Registration.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Registration/Registration.test.tsx index 309c0bfd..0b7a1a14 100644 --- a/src/test/Components/CreateImageWizardV2/steps/Registration/Registration.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/Registration/Registration.test.tsx @@ -70,6 +70,7 @@ const goToReviewStep = async () => { await clickNext(); await clickNext(); await clickNext(); + await clickNext(); await enterBlueprintName(); await clickNext(); }; @@ -78,6 +79,7 @@ describe('registration request generated correctly', () => { const imageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'image-installer', + snapshot_date: '', upload_request: { options: {}, type: 'aws.s3', diff --git a/src/test/Components/CreateImageWizardV2/steps/Repositories/Repositories.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Repositories/Repositories.test.tsx index e7c274a4..aad33486 100644 --- a/src/test/Components/CreateImageWizardV2/steps/Repositories/Repositories.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/Repositories/Repositories.test.tsx @@ -44,6 +44,7 @@ const goToRepositoriesStep = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File System + await clickNext(); // Snapshot await clickNext(); // Custom repositories }; diff --git a/src/test/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.test.tsx b/src/test/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.test.tsx new file mode 100644 index 00000000..7b9f4fac --- /dev/null +++ b/src/test/Components/CreateImageWizardV2/steps/Snapshot/Snapshot.test.tsx @@ -0,0 +1,160 @@ +import { screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { CREATE_BLUEPRINT } from '../../../../../constants'; +import { + CreateBlueprintRequest, + CustomRepository, + Repository, +} from '../../../../../store/imageBuilderApi'; +import { clickNext } from '../../../../testUtils'; +import { + blueprintRequest, + clickRegisterLater, + enterBlueprintName, + interceptBlueprintRequest, + render, +} from '../../wizardTestUtils'; + +jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({ + useChrome: () => ({ + auth: { + getUser: () => { + return { + identity: { + internal: { + org_id: 5, + }, + }, + }; + }, + }, + isBeta: () => true, + isProd: () => true, + getEnvironment: () => 'prod', + }), +})); + +const goToSnapshotStep = async () => { + const bareMetalCheckBox = await screen.findByRole('checkbox', { + name: /bare metal installer checkbox/i, + }); + await userEvent.click(bareMetalCheckBox); + await clickNext(); // Registration + await clickRegisterLater(); + await clickNext(); // OpenSCAP + await clickNext(); // File System + await clickNext(); +}; + +const goToReviewStep = async () => { + await clickNext(); // Repositories step + await clickNext(); // Additional packages + await clickNext(); + await clickNext(); // Details + await enterBlueprintName(); + await clickNext(); +}; + +const selectFirstRepository = async () => { + await userEvent.click( + await screen.findByRole('checkbox', { name: /select row 0/i }) + ); +}; + +const selectUseSnapshot = async () => { + await userEvent.click( + await screen.findByRole('radio', { name: /Use a snapshot/i }) + ); +}; + +const updateDatePickerWithValue = async (date: string) => { + await userEvent.type( + await screen.findByRole('textbox', { name: /Date picker/i }), + date + ); +}; + +const clickContentDropdown = async () => { + await userEvent.click( + ( + await screen.findAllByRole('button', { name: /Content/i }) + )[1] + ); +}; + +const getSnapshotMethodElement = async () => + await screen.findByRole('button', { name: /Snapshot method/i }); + +describe('repository snapshot tab - ', () => { + const expectedPayloadRepositories: Repository[] = [ + { + baseurl: 'http://valid.link.to.repo.org/x86_64/', + check_gpg: true, + check_repo_gpg: false, + gpgkey: + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----', + rhsm: false, + }, + ]; + + const expectedCustomRepositories: CustomRepository[] = [ + { + baseurl: ['http://valid.link.to.repo.org/x86_64/'], + check_gpg: true, + check_repo_gpg: false, + gpgkey: [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----', + ], + id: 'ae39f556-6986-478a-95d1-f9c7e33d066c', + name: '01-test-valid-repo', + }, + ]; + + test('select use a snapshot with 1 repo selected', async () => { + await render(); + await goToSnapshotStep(); + await selectUseSnapshot(); + await updateDatePickerWithValue('04/22/2024'); + await clickNext(); // To repositories step + await selectFirstRepository(); + await goToReviewStep(); + await clickContentDropdown(); + + const snapshotMethodElement = await getSnapshotMethodElement(); + // Check date was recorded correctly + expect(snapshotMethodElement).toHaveTextContent('State as of 04/22/2024'); + // Check that the button is clickable (has 1 repo selected) + expect(snapshotMethodElement).toHaveAttribute('aria-disabled', 'false'); + + // Check the date was passed correctly to the blueprint + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + blueprintRequest.image_requests[0].snapshot_date = '2024-04-22'; + + const expectedRequest: CreateBlueprintRequest = { + ...blueprintRequest, + customizations: { + custom_repositories: expectedCustomRepositories, + payload_repositories: expectedPayloadRepositories, + }, + }; + + expect(receivedRequest).toEqual(expectedRequest); + }); + + test('select use a snapshot with no repos selected', async () => { + await render(); + await goToSnapshotStep(); + await selectUseSnapshot(); + await updateDatePickerWithValue('04/22/2024'); + await clickNext(); // To repositories step + await goToReviewStep(); + await clickContentDropdown(); + + const snapshotMethodElement = await getSnapshotMethodElement(); + // Check date was recorded correctly + expect(snapshotMethodElement).toHaveTextContent('No repositories selected'); + // Check that the button is clickable (has 1 repo selected) + expect(snapshotMethodElement).toHaveAttribute('aria-disabled', 'true'); + }); +}); diff --git a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Aws/AwsTarget.test.tsx b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Aws/AwsTarget.test.tsx index 0a3fd98d..3ae6198d 100644 --- a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Aws/AwsTarget.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Aws/AwsTarget.test.tsx @@ -43,6 +43,7 @@ const goToReview = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File system customization + await clickNext(); // Snapshot repositories await clickNext(); // Custom repositories await clickNext(); // Additional packages await clickNext(); // Details @@ -93,6 +94,7 @@ describe('aws image type request generated correctly', () => { const expectedImageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'aws', + snapshot_date: '', upload_request: { options: { share_with_sources: ['123'], @@ -119,6 +121,7 @@ describe('aws image type request generated correctly', () => { const expectedImageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'aws', + snapshot_date: '', upload_request: { options: { share_with_accounts: ['123123123123'], diff --git a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Azure/AzureTarget.test.tsx b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Azure/AzureTarget.test.tsx index d17096a5..1b85cc13 100644 --- a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Azure/AzureTarget.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/Azure/AzureTarget.test.tsx @@ -43,6 +43,7 @@ const goToReview = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File system customization + await clickNext(); // Snapshot repositories await clickNext(); // Custom repositories await clickNext(); // Additional packages await clickNext(); // Details @@ -120,6 +121,7 @@ describe('azure image type request generated correctly', () => { const expectedImageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'azure', + snapshot_date: '', upload_request: { options: { source_id: '666', @@ -150,6 +152,7 @@ describe('azure image type request generated correctly', () => { const expectedImageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'azure', + snapshot_date: '', upload_request: { type: 'azure', options: { diff --git a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/GCPTarget.test.tsx b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/GCPTarget.test.tsx index 3aec5ed4..156e85df 100644 --- a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/GCPTarget.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/GCPTarget.test.tsx @@ -44,6 +44,7 @@ const goToReview = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File system customization + await clickNext(); // Snapshot repositories await clickNext(); // Custom repositories await clickNext(); // Additional packages await clickNext(); // Details diff --git a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/TargetEnvironment.test.tsx b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/TargetEnvironment.test.tsx index f7ddb284..b299c43f 100644 --- a/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/TargetEnvironment.test.tsx +++ b/src/test/Components/CreateImageWizardV2/steps/TargetEnvironment/TargetEnvironment.test.tsx @@ -120,6 +120,7 @@ const goToReviewStep = async () => { await clickRegisterLater(); await clickNext(); // OpenSCAP await clickNext(); // File system customization + await clickNext(); // Snapshots await clickNext(); // Custom repositories await clickNext(); // Additional packages await clickNext(); // Details diff --git a/src/test/Components/CreateImageWizardV2/wizardTestUtils.tsx b/src/test/Components/CreateImageWizardV2/wizardTestUtils.tsx index e6df2a61..d8f1259e 100644 --- a/src/test/Components/CreateImageWizardV2/wizardTestUtils.tsx +++ b/src/test/Components/CreateImageWizardV2/wizardTestUtils.tsx @@ -41,6 +41,7 @@ const routes = [ export const imageRequest: ImageRequest = { architecture: 'x86_64', image_type: 'image-installer', + snapshot_date: '', upload_request: { options: {}, type: 'aws.s3', diff --git a/src/test/fixtures/blueprints.ts b/src/test/fixtures/blueprints.ts index e0a557c0..51a658ef 100644 --- a/src/test/fixtures/blueprints.ts +++ b/src/test/fixtures/blueprints.ts @@ -144,6 +144,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = { { architecture: 'x86_64', image_type: 'aws', + snapshot_date: '', upload_request: { type: 'aws', options: { @@ -165,6 +166,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = { { architecture: 'x86_64', image_type: 'aws', + snapshot_date: '', upload_request: { type: 'aws', options: { @@ -186,6 +188,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = { { architecture: 'x86_64', image_type: 'gcp', + snapshot_date: '', upload_request: { type: 'gcp', options: { diff --git a/src/test/fixtures/features.ts b/src/test/fixtures/features.ts new file mode 100644 index 00000000..14c962ff --- /dev/null +++ b/src/test/fixtures/features.ts @@ -0,0 +1,16 @@ +import { ListFeaturesApiResponse } from '../../store/contentSourcesApi'; + +export const mockedFeatureResponse: ListFeaturesApiResponse = { + admintasks: { + enabled: true, + accessible: false, + }, + newrepositoryfiltering: { + enabled: false, + accessible: false, + }, + snapshots: { + enabled: true, + accessible: true, + }, +}; diff --git a/src/test/fixtures/snapshots.ts b/src/test/fixtures/snapshots.ts new file mode 100644 index 00000000..8b01ddf1 --- /dev/null +++ b/src/test/fixtures/snapshots.ts @@ -0,0 +1,74 @@ +export const mockSourcesPackagesResults = { + data: [ + { + repository_uuid: '893dbe59-c473-4933-b23c-be6ae806e48d', + is_after: false, + match: { + uuid: '8e1165f2-95f6-4046-a593-6858b34306a3', + created_at: '2024-04-10T15:43:29.95512Z', + repository_path: + '00db8641/893dbe59-c473-4933-b23c-be6ae806e48d/d22d7840-ef14-4420-bccf-408de7949ff1', + content_counts: { + 'rpm.advisory': 2, + 'rpm.package': 8, + 'rpm.packagecategory': 1, + 'rpm.packagegroup': 2, + }, + added_counts: { + 'rpm.advisory': 2, + 'rpm.package': 8, + 'rpm.packagecategory': 1, + 'rpm.packagegroup': 2, + }, + removed_counts: {}, + url: '', + }, + }, + { + repository_uuid: '50467940-7d8e-4eee-a3bd-f8a2556c406c', + is_after: false, + match: { + uuid: 'cc0e882b-e988-4b9a-844a-54d47cb5a9a9', + created_at: '2024-04-10T15:50:59.368233Z', + repository_path: + '00db8641/50467940-7d8e-4eee-a3bd-f8a2556c406c/a1f85ae7-22ac-4e28-8ae3-f8cb249fa5b7', + content_counts: { + 'rpm.advisory': 2, + 'rpm.package': 8, + }, + added_counts: { + 'rpm.advisory': 2, + 'rpm.package': 8, + }, + removed_counts: {}, + url: '', + }, + }, + { + repository_uuid: 'b805848d-1918-4861-9f63-c72f83cbd018', + is_after: false, + match: { + uuid: 'e65b258d-d571-4411-8955-0214fd396aa0', + created_at: '2024-04-17T00:42:07.045744Z', + repository_path: + '00db8641/b805848d-1918-4861-9f63-c72f83cbd018/78335df8-d5e6-4d65-a32f-80a09e6fc17d', + content_counts: { + 'rpm.advisory': 4565, + 'rpm.package': 20851, + 'rpm.packagecategory': 1, + 'rpm.packageenvironment': 1, + 'rpm.packagegroup': 21, + }, + added_counts: { + 'rpm.advisory': 4565, + 'rpm.package': 20851, + 'rpm.packagecategory': 1, + 'rpm.packageenvironment': 1, + 'rpm.packagegroup': 21, + }, + removed_counts: {}, + url: '', + }, + }, + ], +}; diff --git a/src/test/jest.setup.js b/src/test/jest.setup.js index 48df4988..121e16c4 100644 --- a/src/test/jest.setup.js +++ b/src/test/jest.setup.js @@ -1,4 +1,6 @@ import 'whatwg-fetch'; +//Needed for correct jest extends types +import '@testing-library/jest-dom'; import failOnConsole from 'jest-fail-on-console'; import { server } from './mocks/server'; diff --git a/src/test/mocks/handlers.js b/src/test/mocks/handlers.js index b00de0e8..6dc4815f 100644 --- a/src/test/mocks/handlers.js +++ b/src/test/mocks/handlers.js @@ -25,6 +25,7 @@ import { mockCloneStatus, mockStatus, } from '../fixtures/composes'; +import { mockedFeatureResponse } from '../fixtures/features'; import { distributionOscapProfiles, oscapCustomizations, @@ -60,6 +61,15 @@ export const handlers = [ const { search } = await req.json(); return res(ctx.status(200), ctx.json(mockSourcesPackagesResults(search))); }), + rest.get(`${CONTENT_SOURCES_API}/features/`, async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(mockedFeatureResponse)); + }), + rest.post( + `${CONTENT_SOURCES_API}/snapshots/for_date/`, + async (req, res, ctx) => { + return res(ctx.status(200), ctx.json(mockSourcesPackagesResults)); + } + ), rest.get(`${IMAGE_BUILDER_API}/packages`, (req, res, ctx) => { const search = req.url.searchParams.get('search'); return res(ctx.status(200), ctx.json(mockPackagesResults(search)));