diff --git a/web/src/assets/styles/app.scss b/web/src/assets/styles/app.scss index 3ade7e3ad8..dab621c696 100644 --- a/web/src/assets/styles/app.scss +++ b/web/src/assets/styles/app.scss @@ -1,45 +1,11 @@ -// Make proposal actions compact -.proposal-actions li + li { - margin-block-start: 0; -} - -.proposal-action--delete { - font-weight: bold; -} - -// Align the expandable-actions with the actions list -// See https://www.patternfly.org/components/list#css-variables -.expandable-actions { - // --pf-v5-c-list--PaddingLeft - --pf-v5-c-list--nested--MarginLeft - --pf-v5-c-list--m-icon-lg__item-icon-MinWidth - margin-inline-start: calc(var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var(--pf-v5-global--icon--FontSize--sm)); -} - -.expandable-actions > div { - margin-block-start: 0; -} - -// Using a "selected-product" CSS class because sadly we cannot use -// ".pf-v5-c-card:has(> input[type="radio"]:checked)" yet -// -// See: -// - https://drafts.csswg.org/selectors/#relational -// - https://caniuse.com/css-has -.pf-v5-c-card.selected-product { - --pf-v5-c-card--BoxShadow: var(--pf-v5-global--BoxShadow--md); - - .pf-v5-c-radio { - // https://drafts.csswg.org/css-ui/#widget-accent - // https://caniuse.com/mdn-css_properties_accent-color - accent-color: var(--color-primary-darkest); - } - - .pf-v5-c-radio__label { - color: var(--color-primary); - font-weight: bold; +// Better alignment for expandable section with a sibling list +ul.pf-v5-c-list + div.pf-v5-c-expandable-section { + > button { + margin-inline-start: calc(var(--pf-v5-global--spacer--lg) - var(--pf-v5-global--spacer--sm) - var(--pf-v5-global--icon--FontSize--sm)); } - .pf-v5-c-radio__description { - color: var(--color-primary-darkest); + > div { + margin-block-start: 0; } } @@ -56,97 +22,6 @@ button.remove-link:hover { color: var(--pf-v5-c-button--m-danger--BackgroundColor); } - -button.hidden-popover-button { - visibility: hidden; - display: inline; -} - -.wifi-network-menu button.pf-v5-c-dropdown__toggle { - padding-right: 0; -} - -.keep-words { - word-break: keep-all; -} - -button.kebab-toggler { - padding-right: 0; - - svg { - vertical-align: middle; - } -} - -.volumes-list { - .pf-v5-c-label { - margin-inline-end: 5px; - } -} - -.pattern-container { - display: grid; - grid-template-columns: 16px auto; - grid-template-rows: auto auto; - gap: 0.2em 1em; - grid-auto-flow: row; - grid-template-areas: - "checkbox label" - "empty summary"; - margin-bottom: 1em; - padding: 0.5em; - border-radius: 5px; -} - -.pattern-container:hover { - background-color: #eee; -} - -.pattern-label { - display: grid; - grid-template-columns: 32px auto; - grid-template-rows: auto; - gap: 0 1em; - grid-auto-flow: row; - grid-template-areas: "label-icon label-text"; - grid-area: label; -} - -.pattern-label-icon { - grid-area: label-icon; - align-self: center; -} - -.pattern-label-text { - grid-area: label-text; - font-size: 110%; - font-weight: bold; - justify-self: start; - align-self: center; -} - -.pattern-summary { - grid-area: summary; - color: #666; -} - -.pattern-checkbox { - grid-area: checkbox; - justify-self: center; - align-self: center; -} - -.pattern-group-name { - font-size: 120%; -} - -.locale-container { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0 1em; - width: 100%; -} - .first-username-dropdown { position: absolute; width: 100%; diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index e0ec9c93b6..f580648b72 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -2,142 +2,6 @@ // In the future we might add different section layouts by using data-variant attribute // or similar strategy -// Custom selection list -.selection-list > * { - border: 1px solid #eee; - transition: - font-size 0.15s ease-in-out, - font-weight 0.25s ease-in-out, - margin-block 0.15s ease-in-out, - box-shadow 0.35s ease-in-out; - - margin-block-start: -2px; -} - -.selection-list .header { - border-block-end: 1px solid #eee; - padding: var(--spacer-normal); -} - -.selection-list .content { - padding: var(--spacer-normal); -} - -.selection-list [data-state="focused"] { - margin-block: 20px; - box-shadow: 0 0 6px rgb(0 0 0 / 16%), 0 6px 12px rgb(0 0 0 / 32%); -} - -.selection-list [data-state="unstyled"] { - border: 0; -} - -[data-type="agama/sidebar"] { - /** Override the header background, see styles/layout.scss */ - --agama-header-bg: var(--color-primary-lighter); - - position: absolute; - padding: 0; - right: 0; - z-index: 1000; - inline-size: 70%; - min-inline-size: min-content; - box-shadow: -10px 10px 20px 0 var(--color-primary); - - header { - --focus-color: var(--color-primary-darkest); - } - - footer { - border-top: 1px solid var(--color-gray); - } - - a, button { - font-size: 16px; - font-weight: var(--fw-bold); - text-decoration: underline; - text-underline-offset: 2px; - padding-block: 0; - - &:hover { - color: var(--color-link-hover); - text-decoration: underline; - - svg { - color: var(--color-link); - } - } - - svg { - color: var(--color-link); - vertical-align: text-bottom; - margin-block-end: -2px; - } - } - - a { - margin-inline-start: var(--pf-v5-global--spacer--md); - - // Keep links and buttons labels aligned by adding the same margin than - // .pf-v5-c-button__icon.pf-m-start - svg { - margin-inline-end: var(--pf-v5-global--spacer--xs); - } - } - - // Remove not wanted PatternFly padding left on a loading link - button.pf-m-progress { - --pf-v5-c-button--m-progress--PaddingLeft: var(--pf-v5-global--spacer--md); - } - - button.pf-m-progress + div { - padding-inline-start: calc(var(--pf-v5-global--spacer--md)); - } - - &[data-state="hidden"] { - transition: all 0.04s ease-in-out; - inline-size: 0; - min-inline-size: 0; - box-shadow: none; - } - - &[data-state="visible"] { - transition: all 0.2s ease-in-out; - } -} - - -.disclosure > button { - margin-inline-start: var(--pf-v5-global--spacer--md); - display: inline-flex; - align-items: center; - // Keep links and buttons labels aligned by adding the same margin than - // .pf-v5-c-button__icon.pf-m-start - svg { - margin-inline-end: var(--pf-v5-global--spacer--xs); - transition: transform 0.2s ease-in-out; - } - - &[aria-expanded="true"] { - svg { - transform: rotate(90deg); - } - } - - &[aria-expanded="false"] + div { - display: none; - visibility: hidden; - } -} - -.disclosure > div { - margin-inline-start: calc( - var(--pf-v5-global--spacer--md) + 12px // half of the icon size; - ); - border-inline-start: 1px solid var(--color-primary-lighter); - padding-block: var(--spacer-small); -} - // raw file content with formatting similar to
.filecontent { font-family: var(--ff-code); @@ -359,72 +223,6 @@ ul[data-items-type="agama/patterns"] { } } -[data-type="agama/tag"] { - font-size: var(--fs-small); - - &[data-variant="teal"] { - color: var(--color-teal); - } - - &[data-variant="orange"] { - color: var(--color-orange); - } - - &[data-variant="gray-highlight"] { - padding: var(--spacer-smaller); - color: var(--color-gray-darkest); - background: var(--color-gray); - border: 1px solid var(--color-gray-dark); - border-radius: 5px; - margin-inline-start: var(--spacer-smaller); - } -} - -[data-type="agama/controlled-panels"] { - [data-type="agama/option"] { - label, input { - cursor: pointer; - } - - label { - display: flex; - gap: var(--spacer-smaller); - } - } - - [data-variant="buttons"] { - input { position: absolute; opacity: 0 } - - label { - border: 1px solid var(--color-primary); - padding: var(--spacer-small); - gap: var(--spacer-small); - border-radius: var(--spacer-smaller); - position: relative; - - &:has(input:checked) { - background: var(--color-primary); - color: white; - } - - &:has(input:focus-visible) { - // outline: 1px dotted; - // outline-offset: 0.25rem; - box-shadow: 0 0 0 3px var(--focus-color); - } - } - - [data-type="agama/option"]:not(:last-child) { - border-inline-end: 2px solid var(--color-gray-darker); - padding-inline-end: var(--spacer-small); - } - } - - > div[aria-expanded="false"] { - display: none; - } -} - table[data-type="agama/tree-table"] { th:first-child { padding-inline-end: var(--spacer-normal); @@ -556,70 +354,6 @@ table.proposal-result { } } -[data-type="agama/options-picker"] { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: var(--spacer-smaller); - - [role="option"] { - cursor: pointer; - border: 1px solid var(--color-gray); - padding: var(--spacer-small); - border-block-end-width: 4px; - - &[aria-selected="true"] { - background: var(--color-gray-light); - border-color: var(--color-primary); - } - - >:first-child { - margin-block-end: var(--spacer-small); - } - - >:last-child { - font-size: var(--fs-small); - } - } -} - -[data-type="agama/reminder"] { - --accent-color: var(--color-primary-lighter); - --inline-margin: calc(var(--header-icon-size) + var(--spacer-small)); - - display: flex; - gap: var(--spacer-small); - margin-inline: var(--inline-margin); - margin-block-end: var(--spacer-normal); - padding: var(--spacer-smaller) var(--spacer-small); - border-inline-start: 3px solid var(--accent-color); - - svg { - fill: var(--accent-color); - } - - h4 { - color: var(--accent-color); - } - - h4 ~ * { - margin-block-start: var(--spacer-small); - } -} - -section [data-type="agama/reminder"] { - margin-inline: 0; -} - -[data-type="agama/reminder"][data-variant="subtle"] { - --accent-color: var(--color-primary); - padding-block: 0; - border-inline-start-width: 1px; - - h4 { - font-size: var(--fs-normal); - } -} - [role="dialog"] { section:not([class^="pf-c"]) { > svg:first-child { @@ -633,20 +367,6 @@ section [data-type="agama/reminder"] { } } -.tpm-hint { - container-type: inline-size; - container-name: tpm-info; - text-align: start; - - .pf-v5-c-alert__title { - margin-block-end: var(--spacer-small); - } - - .pf-v5-c-alert__description { - max-inline-size: 100%; - } -} - [data-type="agama/expandable-selector"] { // The expandable selector is built on top of PF/Table#expandable // Let's tweak some styles @@ -669,87 +389,3 @@ section [data-type="agama/reminder"] { } } } - -[data-type="agama/field"] { - > div:first-child { - font-size: var(--fs-large); - - button { - padding-inline: 0; - } - - button:hover { - color: var(--color-link-hover); - fill: var(--color-link-hover); - } - - button b, button:hover b { - text-decoration: underline; - text-underline-offset: var(--spacer-smaller); - } - - div.pf-v5-c-skeleton { - display: inline-block; - vertical-align: middle; - height: 1.5ex; - } - } - - > div:nth-child(n+2) { - margin-inline-start: calc(var(--icon-size-s) + 1ch); - } - - > div:nth-child(2) { - color: gray; - font-size: var(--fs-medium); - } - - > div:nth-child(n+3) { - margin-block-start: var(--spacer-small); - } - - &.highlighted > div:last-child { - --spacing: calc(var(--icon-size-s) / 2); - margin-inline: var(--spacing); - padding-inline: var(--spacing); - border-inline-start: 2px solid; - } - - &.highlighted.on > div:last-child { - border-color: var(--color-link-hover); - } - - &.highlighted.off > div:last-child { - border-color: var(--color-gray-darker); - } - - &.on { - button:not(.password-toggler) { - fill: var(--color-link-hover); - } - } - - hr { - margin-block: var(--spacer-normal); - border: 0; - border-bottom: thin dashed var(--color-gray); - } -} - -[data-type="agama/field"] button.pf-v5-c-menu-toggle.pf-m-plain { - padding: 0; -} - -[data-type="agama/field"] .pf-v5-c-menu__list { - padding: calc(var(--spacer-smaller) / 2) 0; - margin: 0; -} - -#boot-form { - legend { - label { - font-size: var(--fs-large); - font-weight: bold; - } - } -} diff --git a/web/src/assets/styles/composition.scss b/web/src/assets/styles/composition.scss index 09be3715ef..eebf2a6c1d 100644 --- a/web/src/assets/styles/composition.scss +++ b/web/src/assets/styles/composition.scss @@ -32,13 +32,7 @@ } } -[data-state="reversed"] { - flex-direction: row-reverse; -} - body > div[inert], -body > div[aria-hidden="true"], -div[data-type="agama/page"] > [inert], -div[data-type="agama/page"] > [aria-hidden="true"] { +body > div[aria-hidden="true"] { filter: grayscale(1) blur(2px); } diff --git a/web/src/assets/styles/index.scss b/web/src/assets/styles/index.scss index 9c4e142efa..e97db6b414 100644 --- a/web/src/assets/styles/index.scss +++ b/web/src/assets/styles/index.scss @@ -6,7 +6,6 @@ // TODO: merge app and global @use "~/assets/styles/global.scss"; @use "~/assets/styles/app.scss"; -@use "~/assets/styles/layout.scss"; @use "~/assets/styles/utilities.scss"; @use "~/assets/styles/composition.scss"; @use "~/assets/styles/blocks.scss"; diff --git a/web/src/assets/styles/layout.scss b/web/src/assets/styles/layout.scss deleted file mode 100644 index d8a7034ad5..0000000000 --- a/web/src/assets/styles/layout.scss +++ /dev/null @@ -1,84 +0,0 @@ -@use "~/assets/styles/utilities.scss"; - -[data-layout="agama/base"] { - --agama-header-bg: var(--color-primary); - - @extend .shadow; - display: grid; - block-size: 100dvh; - background: white; - overflow: hidden; - grid-template-columns: 1fr; - grid-template-rows: 60px 1fr 70px; - grid-template-areas: - "header" - "body" - "footer" - ; - - > * { - padding: var(--spacer-small); - } - - > header { - @extend .bottom-shadow; - grid-area: header; - display: flex; - align-items: center; - justify-content: space-between; - background: var(--agama-header-bg); - color: white; - fill: white; - - h1 { - display: grid; - align-items: center; - gap: var(--spacer-small); - grid-template-columns: var(--header-icon-size) 1fr; - grid-template-areas: - "icon text" - ; - - svg { - grid-area: icon; - block-size: var(--header-icon-size); - inline-size: var(--header-icon-size); - } - - span { - grid-area: text; - } - } - } - - main { - grid-area: body; - overflow: auto; - padding-block: var(--spacer-normal); - container-type: inline-size; - container-name: agama-page-content; - } - - footer { - grid-area: footer; - @extend .top-shadow; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - align-items: center; - - img { - inline-size: 30%; - max-inline-size: 150px; - } - } -} - -[data-type="agama/header-actions"] { - display: flex; - gap: var(--spacer-small); -} - -[data-variant="flip-X"] { - transform: scaleX(-1); -} diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index da930f63e1..4cf2b70cd6 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -67,25 +67,6 @@ --pf-v5-c-button--m-secondary--Color: var(--color-link-hover); } -// Force a separation between PF/EmptyStateBody paragraph without needing -// either: add the .pf-v5-c-content class nor wrapping PF/Text into -// PF/TextContent -.pf-v5-c-empty-state__body p:not(:last-child) { - margin-block-end: var(--pf-v5-global--spacer--md); -} - -// Do not add block padding to empty state inside a table/column -table td > .pf-v5-c-empty-state { - --pf-v5-c-empty-state--PaddingTop: 0; - --pf-v5-c-empty-state--PaddingBottom: 0; -} - -// Fix single-line sub-progress miss-alignment -.pf-v5-c-progress.pf-m-singleline .pf-v5-c-progress__bar { - grid-row: 1/3; - grid-column: 1/3; -} - .pf-v5-c-modal-box__body { padding-block: var(--pf-v5-c-modal-box__body--PaddingTop); } diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss index 9e8030b5e9..980c327205 100644 --- a/web/src/assets/styles/utilities.scss +++ b/web/src/assets/styles/utilities.scss @@ -1,7 +1,3 @@ -.justify-between { - justify-content: space-between; -} - // Sadly, Firefox does not support :has pseudo-selector yet. // See @components/layout/Center documentation. // @@ -19,25 +15,6 @@ inline-size: 100%; } -.horizontally-centered { - inline-size: 100%; - margin-inline: 0 auto; - text-align: center; -} - -.title { - font-size: var(--fs-large); - font-weight: var(--fw-bold); -} - -.bold { - font-weight: bold; -} - -.fs-small { - font-size: var(--fs-small); -} - // Utility classes for sizing icons .icon-xxxs { block-size: var(--icon-size-xxxs); @@ -74,10 +51,6 @@ inline-size: var(--icon-size-xxxl); } -.color-warn { - color: var(--color-warn); -} - .color-success { color: var(--color-success); fill: var(--color-success); @@ -87,85 +60,10 @@ inline-size: 100%; } -.full-size { - width: 100%; - height: 100%; -} - -.transform-on-hover { - transition: all 0.2s ease-in-out; - - &:hover { - transform: scale(1.4); - color: var(--color-primary-darkest); - } -} - -.gradient-border-bottom { - border-bottom: 1px solid #efefef; - border-image: linear-gradient( - var(--gradient-border-angle), - var(--gradient-border-start-color), - var(--gradient-border-end-color) - ) 1; -} - -.visually-hidden { - border: 0; - clip: rect(0 0 0 0); - height: auto; - margin: 0; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - white-space: nowrap; -} - -.shadow { - box-shadow: 0 0 10px 0 var(--color-gray-darker); -} - -.top-shadow { - box-shadow: 0 5px 10px 0 var(--color-gray-darker); -} - -.bottom-shadow { - box-shadow: 0 -3px 10px 0 var(--color-gray-darker); -} - -.plain-control { - position: relative; - background: none; - border: none; -} - -.plain-button { - border: none; - background: none; - color: inherit; - font: inherit; - padding: 0; - text-align: start; -} - -.inline-flex-button{ - @extend .plain-button; - display: inline-flex; - align-items: center; - gap: 0.7ch; - text-decoration: underline; -} - -.p-0 { - padding: 0; -} - .block-size-auto { block-size: auto; } - .inline-size-auto { inline-size: auto; } @@ -218,17 +116,6 @@ } } -.large { - /** block-size fallbacks **/ - height: 95dvh; - width: 95dvw; - max-width: calc(var(--ui-max-inline-size) + var(--spacer-large)); - /** END block-size fallbacks **/ - block-size: 95dvh; - inline-size: 95dvw; - max-inline-size: calc(var(--ui-max-inline-size) + var(--spacer-large)) -} - .scrollbox { background: linear-gradient(#fff 33%, rgb(255 255 255 / 0%)), @@ -241,10 +128,6 @@ background-size: 100% 48px, 100% 48px, 100% 16px, 100% 16px; } -.height-75 { - height: 75dvh; -} - // FIXME: drop as soon as Tip component gets rethought / refactored .label-tip .pf-v5-c-label__text { display: flex; diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss index 7c79ff96c2..7e35e4e433 100644 --- a/web/src/assets/styles/variables.scss +++ b/web/src/assets/styles/variables.scss @@ -40,49 +40,19 @@ --icon-size-xxl: 5rem; --icon-size-xxxl: 10rem; - --wrapper-padding: var(--spacer-small); - --wrapper-background: white; - --color-primary: #0c322c; - --color-primary-lighter: #30ba78; --color-gray-light: #fcfcfc; --color-gray: #f2f2f2; --color-gray-dark: #efefef; // Fog - --color-gray-darker: #999; - --color-gray-darkest: #333; --color-gray-dimmed: #888; - --color-gray-dimmest: #666; - --color-teal: #279c9c; - --color-blue: #0d4ea6; - --color-orange: #e86427; + --color-success: #30ba78; --color-link: #0c322c; --color-link-hover: #30ba78; - --color-button-primary: var(--color-link); --color-button-primary-hover: var(--color-link-hover); - --color-button-plain-link: var(--color-link); --color-button-plain-link-hover: var(--color-link-hover); - --color-background-primary: var(--color-primary); - --color-background-secondary: var(--color-gray-dark); - - --color-text-primary: var(--color-primary); - --color-text-secondary: var(--color-gray-dark); - - --color-success: #30ba78; - --color-warn: #d4351c; // #FE7C3F; // Persimmon - --focus-color: #00b2e2; //cerulean 500 - - --gradient-border-angle: 45deg; - --gradient-border-start-color: var(--color-gray); - --gradient-border-end-color: transparent; - - --header-icon-size: var(--icon-size-m); - --section-icon-size: var(--icon-size); - - --header-block-size: auto; - --footer-block-size: auto; } diff --git a/web/src/components/core/EmptyState.jsx b/web/src/components/core/EmptyState.jsx index 46f7cd9770..32c256aa46 100644 --- a/web/src/components/core/EmptyState.jsx +++ b/web/src/components/core/EmptyState.jsx @@ -22,7 +22,7 @@ // @ts-check import React from "react"; -import { EmptyState, EmptyStateHeader, EmptyStateBody } from "@patternfly/react-core"; +import { EmptyState, EmptyStateHeader, EmptyStateBody, Flex } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; /** @@ -42,10 +42,10 @@ import { Icon } from "~/components/layout"; * @param {string} props.title * @param {IconName} props.icon * @param {string} props.color - * @param {Pick} [props.headingLevel="h4"] + * @param {EmptyStateHeaderProps["headingLevel"]} [props.headingLevel="h4"] * @param {boolean} [props.noPadding=false] * @param {React.ReactNode} props.children - * @param {EmptyStateProps} [props.props] + * @param {EmptyStateProps} [props.rest] * @todo write documentation */ export default function EmptyStateWrapper({ @@ -55,12 +55,12 @@ export default function EmptyStateWrapper({ headingLevel = "h4", noPadding = false, children, - ...props + ...rest }) { - if (noPadding) props.className = [props.className, 'no-padding'].join(" ").trim(); + if (noPadding) rest.className = [rest.className, 'no-padding'].join(" ").trim(); return ( - + ); diff --git a/web/src/components/core/FileViewer.jsx b/web/src/components/core/FileViewer.jsx deleted file mode 100644 index 8fa58990aa..0000000000 --- a/web/src/components/core/FileViewer.jsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useState, useEffect } from "react"; -import { Popup } from "~/components/core"; -import { Alert } from "@patternfly/react-core"; -import { Loading } from "~/components/layout"; -import { _ } from "~/i18n"; - -import cockpit from "../../lib/cockpit"; - -export default function FileViewer({ file, title, onCloseCallback }) { - // the popup is visible - const [isOpen, setIsOpen] = useState(true); - // error message for failed load - const [error, setError] = useState(null); - // the file content - const [content, setContent] = useState(""); - // current state - const [state, setState] = useState("loading"); - - useEffect(() => { - // NOTE: reading non-existing files in cockpit does not fail, the result is null - // see https://cockpit-project.org/guide/latest/cockpit-file - cockpit.file(file).read() - .then((data) => { - setState("ready"); - setContent(data); - }) - .catch((data) => { - setState("ready"); - setError(data.message); - }); - }, [file]); - - const close = () => { - setIsOpen(false); - if (onCloseCallback) onCloseCallback(); - }; - - return ( - } /> - {children} + + {children} + - {state === "loading" && - ); -} diff --git a/web/src/components/core/FileViewer.test.jsx b/web/src/components/core/FileViewer.test.jsx deleted file mode 100644 index 6c9b3972d2..0000000000 --- a/web/src/components/core/FileViewer.test.jsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) [2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; - -import { screen, waitFor, within } from "@testing-library/react"; -import { mockGettext, plainRender } from "~/test-utils"; -import { FileViewer } from "~/components/core"; -import cockpit from "../../lib/cockpit"; - -jest.mock("../../lib/cockpit"); - -const readFn = jest.fn(() => new Promise(jest.fn())); - -const fileFn = jest.fn(); -fileFn.mockImplementation(() => { - return { - read: readFn - }; -}); - -cockpit.file.mockImplementation(fileFn); - -// testing data -const file_name = "/testfile"; -const content = "Read file content"; -const title = "YaST Logs"; - -mockGettext(); - -describe("FileViewer", () => { - beforeEach(() => { - readFn.mockResolvedValue(content); - }); - - it("displays the specified file and the title", async () => { - plainRender(} - {(content === null || error) && - - {error} - } -- {content} -- -- -{_("Close")} -); - const dialog = await screen.findByRole("dialog"); - - // the file was read from cockpit - expect(fileFn).toHaveBeenCalledWith(file_name); - expect(readFn).toHaveBeenCalled(); - - within(dialog).getByText(title); - within(dialog).getByText(content); - }); - - it("displays the file name when the title is missing", async () => { - plainRender( ); - const dialog = await screen.findByRole("dialog"); - - within(dialog).getByText(file_name); - }); - - it("closes the popup after clicking the close button", async () => { - const { user } = plainRender( ); - const dialog = await screen.findByRole("dialog"); - const closeButton = within(dialog).getByRole("button", { name: /Close/i }); - - await user.click(closeButton); - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - it("triggers the onCloseCallback after clicking the close button", async () => { - const callback = jest.fn(); - const { user } = plainRender( ); - const dialog = await screen.findByRole("dialog"); - const closeButton = within(dialog).getByRole("button", { name: /Close/i }); - - await user.click(closeButton); - - expect(callback).toHaveBeenCalled(); - }); - - describe("when the file does not exist", () => { - beforeEach(() => { - readFn.mockResolvedValue(null); - }); - - it("displays an error", async () => { - plainRender( ); - const dialog = await screen.findByRole("dialog"); - - await within(dialog).findByText(/cannot read the file/i); - }); - }); - - describe("when the file cannot be read", () => { - beforeEach(() => { - readFn.mockRejectedValue(new Error("read error")); - }); - - it("displays the error message", async () => { - plainRender( ); - const dialog = await screen.findByRole("dialog"); - - await within(dialog).findByText(/read error/i); - }); - }); -}); diff --git a/web/src/components/core/InstallationFinished.jsx b/web/src/components/core/InstallationFinished.jsx index c60b2943b3..a56ece90d5 100644 --- a/web/src/components/core/InstallationFinished.jsx +++ b/web/src/components/core/InstallationFinished.jsx @@ -99,13 +99,15 @@ function InstallationFinished() { icon={ } /> - {_("The installation on your machine is complete.")} -- {usingIguana - ? _("At this point you can power off the machine.") - : _("At this point you can reboot the machine to log in to the new system.")} - - {usingTpm &&} + + {_("The installation on your machine is complete.")} ++ {usingIguana + ? _("At this point you can power off the machine.") + : _("At this point you can reboot the machine to log in to the new system.")} + + {usingTpm &&} + diff --git a/web/src/components/core/Page.jsx b/web/src/components/core/Page.jsx index 6e2ed90862..a6c70af5a0 100644 --- a/web/src/components/core/Page.jsx +++ b/web/src/components/core/Page.jsx @@ -30,7 +30,6 @@ import { PageGroup, PageSection, Stack } from "@patternfly/react-core"; -import { PageMenu } from "~/components/core"; import { _ } from "~/i18n"; import tabsStyles from '@patternfly/react-styles/css/components/Tabs/tabs'; import flexStyles from '@patternfly/react-styles/css/utilities/Flex/flex'; @@ -52,16 +51,6 @@ import flexStyles from '@patternfly/react-styles/css/utilities/Flex/flex'; */ const Actions = ({ children }) => <>{children}>; -/** - * Component for rendering options related to the page. I.e., a menu. - * - * @note it is defined in its own file and then included here under Page.Menu - * "alias". - * - * @see core/PageMenu to know more. - */ -const Menu = PageMenu; - /** * A convenient component representing a Page action * @@ -180,47 +169,6 @@ const CardSection = ({ title, children, ...props }) => { * * * - * @example Using custom actions - *- * - * - * @example- * - * - * - *alert("Are you sure?")}> - * Reset to defaults - * - *Accept - *Using custom actions and a page menu - *- * - * - * @example- * - * - * - * - *- * - *Expert mode - *Help - *- * - *alert("Are you sure?")}> - * Reset to defaults - * - *Accept - *Using a page menu from external component - * ... - * import { UserPageMenu } from "somewhere"; - * ... - *- * - * * @param {object} props * @param {string} [props.icon] - The icon for the page. * @param {string} [props.title="Agama"] - The title for the page. By default it @@ -245,7 +193,6 @@ Page.CardSection = CardSection; Page.Actions = Actions; Page.NextActions = NextActions; Page.Action = Action; -Page.Menu = Menu; Page.MainContent = MainContent; Page.CancelAction = CancelAction; Page.Header = Header; diff --git a/web/src/components/core/PageMenu.jsx b/web/src/components/core/PageMenu.jsx deleted file mode 100644 index 608c9a0384..0000000000 --- a/web/src/components/core/PageMenu.jsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (c) [2023-2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check - -import React, { useState } from 'react'; -import { - Dropdown, DropdownGroup, DropdownItem, DropdownList, - MenuToggle -} from '@patternfly/react-core'; -import { _ } from "~/i18n"; -import { Icon } from "~/components/layout"; - -/** - * Internal component to build the {PageMenu} toggler - * @component - * - * @typedef {object} TogglerBaseProps - * @property {React.Ref- * - * - * } toggleRef - * @property {string} label - * - * @typedef {TogglerBaseProps & import('@patternfly/react-core').MenuToggleProps} TogglerProps - * - * @param {TogglerProps} props - */ -const Toggler = ({ toggleRef, label, onClick, "aria-label": ariaLabel = _(("Show page menu")) }) => { - return ( - - {label} - - ); -}; - -/** - * A group of actions belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdowngroup PF/DropdownGroup} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownGroupProps} props - */ -const Group = ({ children, ...props }) => { - return ( -- - {children} - - ); -}; - -/** - * An option belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdownitem PF/DropdownItem} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownItemProps} props - */ -const Option = ({ children, ...props }) => { - return ( -- {children} - - ); -}; - -/** - * A collection of {Option}s belonging to a {PageMenu} component - * @component - * - * Built on top of {@link https://www.patternfly.org/components/menus/dropdown#dropdownlist PF/DropdownList} - * - * @see {PageMenu} examples. - * - * @param {import('@patternfly/react-core').DropdownListProps} props - */ -const Options = ({ children, ...props }) => { - return ( -- {children} - - ); -}; - -/** - * Component for rendering actions related to a page. - * @component - * - * It consist in a {@link https://www.patternfly.org/components/menus/dropdown PF/Dropdown} - * rendered in the header close to the action for opening the Sidebar. - * - * @note when wrapping it in another component intended to hold all the needed - * logic for building the page menu, it's name must includes the "PageMenu" suffix. - * This is needed to allow core/Page properly work with it. See core/Page component - * for a better understanding. - * - * @see core/Page component. - * - * @exampleUsage example - *- * - * - * @typedef {object} PageMenuProps - * @property {string} [togglerAriaLabel] - * @property {string} label - * @property {React.ReactNode} children - * - * @param {PageMenuProps} props - */ -const PageMenu = ({ togglerAriaLabel, label, children }) => { - const [isOpen, setIsOpen] = useState(false); - - const toggle = () => setIsOpen(!isOpen); - const close = () => setIsOpen(false); - - return ( -- * - *- * - * Reprobe - * - *- * - *- * - * DASD - * - *- * iSCSI - * - *- * - ); -}; - -PageMenu.Group = Group; -PageMenu.Options = Options; -PageMenu.Option = Option; - -export default PageMenu; diff --git a/web/src/components/core/PageMenu.test.jsx b/web/src/components/core/PageMenu.test.jsx deleted file mode 100644 index a20bbf27e5..0000000000 --- a/web/src/components/core/PageMenu.test.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) [2022-2023] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen, waitForElementToBeRemoved } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { PageMenu } from "~/components/core"; - -it("renders the component initially closed", async () => { - plainRender( - } - onSelect={close} - onOpenChange={close} - popperProps={{ minWidth: "150px", position: "right" }} - data-type="agama/page-menu" - > - - {children} - -- - ); - - screen.getByRole("button", { name: "Testing menu" }); - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); -}); - -it("shows and hides the component content on user request", async () => { - const { user } = plainRender( -A dummy action -- - ); - - const toggler = screen.getByRole("button", { name: "Menu toggler" }); - - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); - - await user.click(toggler); - - screen.getByRole("menuitem", { name: "A dummy action" }); - - await user.click(toggler); - await waitForElementToBeRemoved(screen.queryByRole("menuitem", { name: "A dummy action" })); - // NOTE: the above is the same than: - // await waitFor(() => { - // expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); - // }); -}); - -it("hides the component content when user clicks on one of its actions", async () => { - const { user } = plainRender( -<>A dummy action> -- - ); - - const toggler = screen.getByRole("button"); - await user.click(toggler); - const action = screen.getByRole("menuitem", { name: "Section" }); - await user.click(action); - - expect(screen.queryByRole("menuitem", { name: "A dummy action" })).toBeNull(); -}); - -it('closes the component when user clicks outside', async () => { - const { user } = plainRender( - <> -- -<>Section> -<>Page> -<>Exit> -Sibling-- - > - ); - - const toggler = screen.getByRole("button"); - const sibling = screen.getByText("Sibling"); - - // Open the dropdown - await user.click(toggler); - - // Ensure the dropdown is open - screen.getByRole("menuitem", { name: "Option 2" }); - - // Click outside the dropdown - await user.click(sibling); - - // Ensure the dropdown is closed - await waitForElementToBeRemoved(() => screen.queryByRole("menuitem", { name: "Option 2" })); -}); diff --git a/web/src/components/core/Popup.jsx b/web/src/components/core/Popup.jsx index e16fc38413..5779d2fb80 100644 --- a/web/src/components/core/Popup.jsx +++ b/web/src/components/core/Popup.jsx @@ -23,15 +23,9 @@ import React from "react"; import { Button, Modal } from "@patternfly/react-core"; +import { Loading } from "~/components/layout"; import { _ } from "~/i18n"; import { partition } from "~/utils"; -import { Loading } from "~/components/layout"; - -/** - * @typedef {import("@patternfly/react-core").ModalProps} ModalProps - * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps - * @typedef {Omit<>Option 1> -<>Option 2> -} ButtonWithoutVariantProps - */ /** * @typedef {import("@patternfly/react-core").ModalProps} ModalProps diff --git a/web/src/components/core/Selector.jsx b/web/src/components/core/Selector.jsx deleted file mode 100644 index 5e5763e6e1..0000000000 --- a/web/src/components/core/Selector.jsx +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check -import React from "react"; -import { _ } from "~/i18n"; -import { noop } from "~/utils"; - -/** - * @callback onSelectionChangeCallback - * @param {Array } selection - ids of selected options - */ - -/** - * Agama component for building a selector - * - * @example Usage example - * const options = [ - * { id: "es_ES", country: "Spain", label: "Spanish" }, - * { id: "cs_CZ", country: "Czechia", label: "Czech" }, - * { id: "de_DE", country: "Germany", label: "German" }, - * { id: "en_GB", country: "United Kingdom", label: "English" } - * ]; - * - * const selectedIds = ["es_ES", "en_GB"]; - * - * const renderFn = ({ label, country }) =>{label} - {country}; - * - * return ( - *changePreferredLocales(selection)} - * /> - * ); - * - * @param {object} props - component props - * @param {string} [props.id] - Id attribute for selector. - * @param {boolean} [props.isMultiple=false] - Whether the selector should allow multiple selection. - * @param {Array