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 (
-    
+    
       }
       />
       
-        {children}
+        
+          {children}
+        
       
     
   );
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 (
-    
-      {state === "loading" && }
-      {(content === null || error) &&
-        
-          {error}
-        }
-      
- {content} -
- - - {_("Close")} - -
- ); -} 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(); - 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 - * - * - * - * - * alert("Are you sure?")}> - * Reset to defaults - * - * Accept - * - * - * - * @example Using custom actions and a page menu - * - * - * - * - * - * Expert mode - * Help - * - * - * - * - * alert("Are you sure?")}> - * Reset to defaults - * - * Accept - * - * - * - * @example 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. - * - * @example Usage example - * - * - * - * - * Reprobe - * - * - * - * - * - * DASD - * - * - * iSCSI - * - * - * - * - * - * @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 ( - } - onSelect={close} - onOpenChange={close} - popperProps={{ minWidth: "150px", position: "right" }} - data-type="agama/page-menu" - > - - {children} - - - ); -}; - -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( - - A dummy action - - ); - - 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( - - - <>Section - <>Page - - <>Exit - - ); - - 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( - <> -
Sibling
- - <>Option 1 - <>Option 2 - - - ); - - 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} 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} props.options=[] - Item objects to build options. - * @param {function} props.renderOption=noop - Function used for rendering options. - * @param {string} [props.optionIdKey="id"] - Key used for retrieve options id. - * @param {Array<*>} [props.selectedIds=[]] - Identifiers for selected options. - * @param {function} props.autoSelectionCheck=noop - Function used to check if option should be marked as auto selected. - * @param {onSelectionChangeCallback} [props.onSelectionChange=noop] - Callback to be called when the selection changes. - * @param {function|undefined} [props.onOptionClick] - Callback to be called when the selection changes. - * @param {object} [props.props] - Other props sent to the internal selector
    component - */ -const Selector = ({ - id = crypto.randomUUID(), - isMultiple = false, - options = [], - renderOption = noop, - optionIdKey = "id", - selectedIds = [], - autoSelectionCheck = noop, - onSelectionChange = noop, - onOptionClick, - ...props -}) => { - const onOptionClicked = (optionId) => { - if (typeof onOptionClick === "function") return onOptionClick(optionId); - - const alreadySelected = selectedIds.includes(optionId); - - if (!isMultiple) { - !alreadySelected && onSelectionChange([optionId]); - return; - } - - if (alreadySelected) { - onSelectionChange(selectedIds.filter((id) => id !== optionId)); - } else { - onSelectionChange([...selectedIds, optionId]); - } - }; - - return ( -
      - {options.map((option) => { - const optionId = option[optionIdKey]; - const optionHtmlId = `${id}-option-${optionId}`; - const isSelected = selectedIds.includes(optionId); - const isAutoSelected = isSelected && autoSelectionCheck(option); - const onClick = () => onOptionClicked(optionId); - - return ( -
    • -
      -
      - - {isAutoSelected &&
      {_("auto selected")}
      } -
      - {renderOption(option)} -
      -
    • - ); - })} -
    - ); -}; - -export default Selector; diff --git a/web/src/components/core/Selector.test.jsx b/web/src/components/core/Selector.test.jsx deleted file mode 100644 index 96fa778cf9..0000000000 --- a/web/src/components/core/Selector.test.jsx +++ /dev/null @@ -1,153 +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. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import { Selector } from "~/components/core"; - -const onChangeFn = jest.fn(); - -const TestingSelector = ({ isMultiple = false, selectedIds = ["es_ES"], ...props }) => { - const [selected, setSelected] = React.useState(selectedIds); - - onChangeFn.mockImplementation((selection) => setSelected(selection)); - - return ( -
    {option.label} - {option.country}
    } - selectedIds={selected} - onSelectionChange={onChangeFn} - aria-label="Testing selector" - { ...props } - /> - ); -}; - -const MultipleTestingSelector = (props) => ; - -describe("Selector", () => { - it("renders a selector and its options", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - within(selector).getByRole("row", { name: "Spanish - Spain" }); - within(selector).getByRole("row", { name: "English - United Kingdom" }); - }); - - it("uses `id` as key for the option id if `optionIdKey` prop is not given", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - await user.click(option); - expect(onChangeFn).toHaveBeenCalledWith(["en_GB"]); - }); - - it("uses given `optionIdKey` as key for the option id", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - await user.click(option); - expect(onChangeFn).toHaveBeenCalledWith([2]); - }); - - it("sets data-auto-selected attribute to selected options for which `autoSelectionCheck` returns true", () => { - plainRender( - // Forcing a not selected option (en_GB) as auto selected for checking that it is not. - ["es_ES", "en_GB"].includes(o.id)} /> - ); - const spanishRow = screen.getByRole("row", { name: "Spanish - Spain auto selected" }); - const englishRow = screen.getByRole("row", { name: "English - United Kingdom" }); - const spanishOption = within(spanishRow).getByRole("radio"); - const englishOption = within(englishRow).getByRole("radio"); - expect(spanishRow).toHaveAttribute("data-auto-selected"); - expect(spanishOption).toHaveAttribute("data-auto-selected"); - expect(englishRow).not.toHaveAttribute("data-auto-selected"); - expect(englishOption).not.toHaveAttribute("data-auto-selected"); - }); - - describe("when set as single selector", () => { - it("renders a radio input for each option", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - const options = within(selector).getAllByRole("row"); - options.forEach((option) => within(option).getByRole("radio")); - }); - - describe("and user clicks on a selected option", () => { - it("keeps it as selected and does not trigger the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).toHaveAttribute("aria-selected"); - expect(onChangeFn).not.toHaveBeenCalled(); - }); - }); - - describe("and user clicks a not selected option", () => { - it("sets it as selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const initialSelection = screen.getByRole("row", { name: "Spanish - Spain" }); - const nextSelection = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(initialSelection).toHaveAttribute("aria-selected"); - expect(nextSelection).not.toHaveAttribute("aria-selected"); - await user.click(nextSelection); - expect(initialSelection).not.toHaveAttribute("aria-selected"); - expect(nextSelection).toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(["en_GB"]); - }); - }); - }); - - describe("when set as multiple selector", () => { - it("renders a checkbox input for each option", () => { - plainRender(); - const selector = screen.getByRole("grid", { name: "Testing selector" }); - const options = within(selector).getAllByRole("row"); - options.forEach((option) => within(option).getByRole("checkbox")); - }); - - describe("and user clicks on a selected option", () => { - it("sets it as not selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).not.toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(expect.not.arrayContaining(["en_GB"])); - }); - }); - - describe("and user clicks on a not selected option", () => { - it("sets it as selected and triggers the #onSelectionChange callback", async () => { - const { user } = plainRender(); - const option = screen.getByRole("row", { name: "English - United Kingdom" }); - expect(option).not.toHaveAttribute("aria-selected"); - await user.click(option); - expect(option).toHaveAttribute("aria-selected"); - expect(onChangeFn).toHaveBeenCalledWith(expect.arrayContaining(["en_GB"])); - }); - }); - }); -}); diff --git a/web/src/components/core/ServerError.jsx b/web/src/components/core/ServerError.jsx index 27aafd7318..3452081129 100644 --- a/web/src/components/core/ServerError.jsx +++ b/web/src/components/core/ServerError.jsx @@ -23,35 +23,38 @@ import React from "react"; import { EmptyState, EmptyStateIcon, EmptyStateBody, EmptyStateHeader } from "@patternfly/react-core"; import { Center, Icon } from "~/components/layout"; import { Page } from "~/components/core"; +import SimpleLayout from "~/SimpleLayout"; import { _ } from "~/i18n"; import { locationReload } from "~/utils"; +// TODO: refactor, and if possible use a route for it. Related with needed +// changes in src/App.jsx + const ErrorIcon = () => ; function ServerError() { return ( - // TRANSLATORS: page title - -
    - - } - /> - - {_("Please, check whether it is running.")} - - -
    - - - locationReload()}> - {/* TRANSLATORS: button label */} + + +
    + + } + /> + + {_("Please, check whether it is running.")} + + +
    +
    + + {_("Reload")} -
    -
    + + ); } diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 76354cc861..54ad24b00d 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -20,7 +20,6 @@ */ export { default as About } from "./About"; -export { default as PageMenu } from "./PageMenu"; export { default as Description } from "./Description"; export { default as Section } from "./Section"; export { default as FormLabel } from "./FormLabel"; @@ -38,7 +37,6 @@ export { default as SectionSkeleton } from "./SectionSkeleton"; export { default as ListSearch } from "./ListSearch"; export { default as LoginPage } from "./LoginPage"; export { default as LogsButton } from "./LogsButton"; -export { default as FileViewer } from "./FileViewer"; export { default as RowActions } from "./RowActions"; export { default as Page } from "./Page"; export { default as PasswordAndConfirmationInput } from "./PasswordAndConfirmationInput"; @@ -48,7 +46,6 @@ export { default as ProgressText } from "./ProgressText"; export { default as Tip } from "./Tip"; export { default as NumericTextInput } from "./NumericTextInput"; export { default as PasswordInput } from "./PasswordInput"; -export { default as Selector } from "./Selector"; export { default as ServerError } from "./ServerError"; export { default as ExpandableSelector } from "./ExpandableSelector"; export { default as TreeTable } from "./TreeTable"; diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.jsx index 3ecc03369b..086d6b60fe 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.jsx @@ -171,6 +171,8 @@ const PREDEFINED_SIZES = [ * @param {IconName} props.name - Name of the desired icon. * @param {string} [props.className=""] - CSS classes. * @param {IconSize} [props.size] - Size used for both, width and height. + * @param {string} [props.color] - Color for the icon, currently based on PF + * text utils * It can be a CSS unit or one of PREDEFINED_SIZES. * @param {object} [props.otherProps] Other props sent to SVG icon. Please, note * that width and height will be overwritten by the size value if it was given. diff --git a/web/src/components/storage/ProposalActionsDialog.jsx b/web/src/components/storage/ProposalActionsDialog.jsx index 17bcd9484d..89e19d7a8e 100644 --- a/web/src/components/storage/ProposalActionsDialog.jsx +++ b/web/src/components/storage/ProposalActionsDialog.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -29,9 +29,11 @@ const ActionsList = ({ actions }) => { // Some actions (e.g., deleting a LV) are reported as several actions joined by a line break const actionItems = (action, id) => { return action.text.split("\n").map((text, index) => { + const Wrapper = action.delete ? "strong" : "span"; + return ( - - {text} + + {text} ); }); @@ -39,7 +41,7 @@ const ActionsList = ({ actions }) => { const items = actions.map(actionItems).flat(); - return {items}; + return {items}; }; /** @@ -72,7 +74,6 @@ export default function ProposalActionsDialog({ actions = [] }) { isExpanded={isExpanded} onToggle={() => setIsExpanded(!isExpanded)} toggleText={toggleText} - className="expandable-actions" > }