mirror of https://github.com/rancher/dashboard.git
Merge branch 'master' into alert-polish
This commit is contained in:
commit
997feac58d
|
|
@ -24,8 +24,8 @@
|
|||
@import "./global/labeled-input";
|
||||
@import "./global/tooltip";
|
||||
@import "./global/table";
|
||||
@import "./global/select";
|
||||
|
||||
/* @import 'vue-select/src/scss/vue-select.scss'; */
|
||||
@import "./vendor/vue-select";
|
||||
@import "./vendor/vue-js-modal";
|
||||
@import '@/node_modules/xterm/css/xterm.css';
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ TEXTAREA,
|
|||
BUTTON,
|
||||
.btn,
|
||||
.labeled-input,
|
||||
.labeled-select,
|
||||
.unlabeled-select,
|
||||
.checkbox-custom,
|
||||
.radio-custom {
|
||||
&:focus, &.focused {
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ button,
|
|||
//btn sizes
|
||||
.btn-xs,
|
||||
.btn-group-xs > .btn,
|
||||
.btn-xs .btn-label {
|
||||
.btn-xs .btn-label,
|
||||
.btn-xs .dd-button > .vs__dropdown-toggle {
|
||||
padding: $xs-padding;
|
||||
font-size: .65em;
|
||||
font-size: 0.65em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +64,6 @@ button,
|
|||
.btn-group-sm > .btn,
|
||||
.btn-sm .btn-label {
|
||||
padding: $sm-padding;
|
||||
|
||||
}
|
||||
|
||||
.btn-group-sm > .btn {
|
||||
|
|
@ -113,7 +113,6 @@ button,
|
|||
line-height: 0;
|
||||
}
|
||||
|
||||
|
||||
.icon-group i {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ INPUT[type='password'],
|
|||
INPUT[type='number'],
|
||||
INPUT[type='date'],
|
||||
INPUT[type='email'],
|
||||
INPUT[type='search'],
|
||||
INPUT[type='search']:not(.vs__search),
|
||||
INPUT[type='tel'],
|
||||
INPUT[type='url'],
|
||||
SELECT,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
transition-timing-function: ease-in-out;
|
||||
color: var(--input-label);
|
||||
pointer-events: none;
|
||||
z-index: z-index('overContent');
|
||||
i {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
.labeled-select {
|
||||
cursor: text;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
|
||||
.selected {
|
||||
padding-top: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.labeled-select,
|
||||
.unlabeled-select {
|
||||
min-width: 75px;
|
||||
height: 100%;
|
||||
|
||||
.v-select {
|
||||
&.inline {
|
||||
.vs__search {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.vs__dropdown-toggle,
|
||||
.vs__dropdown-toggle > * {
|
||||
background-color: transparent;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.vs__dropdown-menu {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.selected {
|
||||
position: relative;
|
||||
top: 1.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-select.inline.vs--single {
|
||||
&.vs--searching .vs__selected {
|
||||
display: none;
|
||||
}
|
||||
&:not(.vs--searching) {
|
||||
.vs__selected-options {
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
.vs__selected {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-select.inline:not(.vs--single) {
|
||||
margin-bottom: -4px; // targets multi-select tag boxes to make the same size as rows next to it
|
||||
min-height: 30px;
|
||||
|
||||
.vs__selected {
|
||||
min-height: 25px;
|
||||
padding: 0 7px;
|
||||
|
||||
&:not(:only-child) {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.focused {
|
||||
outline: none;
|
||||
border: var(--outline-width) solid var(--outline);
|
||||
box-shadow: none;
|
||||
.v-select {
|
||||
// Can toggle this to get full width dd - maybe make an option?
|
||||
.vs__dropdown-menu {
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unlabeled-select {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius);
|
||||
border: solid var(--outline-width) var(--input-border);
|
||||
color: var(--input-text);
|
||||
|
||||
.vs--single .vs__selected-options {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.v-select {
|
||||
&.inline {
|
||||
height: 100%;
|
||||
|
||||
.vs__dropdown-toggle {
|
||||
height: 100%;
|
||||
}
|
||||
.vs__actions {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.view) {
|
||||
background-color: var(--input-bg);
|
||||
|
||||
&:hover {
|
||||
&,
|
||||
.vs__dropdown-menu {
|
||||
background: var(--input-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -180,134 +180,3 @@
|
|||
transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
|
||||
// load here instead of component so SSR render isn't all wonky
|
||||
.dropdown-button-group {
|
||||
// this matches the top/bottom padding of the default button
|
||||
$trigger-padding: 15px 10px 15px 10px;
|
||||
$xs-trigger-padding: 2px 4px 4px 4px;
|
||||
$sm-trigger-padding: 8px 10px 10px 10px;
|
||||
$lg-trigger-padding: 18px 10px 10px 10px;
|
||||
|
||||
.v-popover {
|
||||
.text-right {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.trigger {
|
||||
height: 100%;
|
||||
.icon-container {
|
||||
height: 100%;
|
||||
padding: $trigger-padding;
|
||||
i {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
&.btn-xs {
|
||||
padding: $xs-trigger-padding;
|
||||
}
|
||||
&.btn-sm {
|
||||
padding: $sm-trigger-padding;
|
||||
}
|
||||
&.btn-lg {
|
||||
padding: $lg-trigger-padding;
|
||||
}
|
||||
&:focus {
|
||||
outline-style: none;
|
||||
box-shadow: none;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
background: var(--tooltip-bg);
|
||||
border: solid thin var(--link-text);
|
||||
color: var(--link-text);
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
|
||||
&>*, .icon-chevron-down {
|
||||
color: var(--primary);
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.button-divider {
|
||||
border-right: 1px solid var(--link-text);
|
||||
}
|
||||
|
||||
&.bg-primary:hover {
|
||||
background: var(--accent-btn-hover);
|
||||
}
|
||||
|
||||
&.one-action {
|
||||
position: relative;
|
||||
&>.btn {
|
||||
padding: 15px 35px 15px 15px;
|
||||
}
|
||||
.v-popover{
|
||||
.trigger{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
BUTTON {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover {
|
||||
border: none;
|
||||
}
|
||||
.tooltip {
|
||||
&[x-placement^="bottom"] {
|
||||
.tooltip-arrow {
|
||||
border-bottom-color: var(--dropdown-border);
|
||||
|
||||
&:after {
|
||||
border-bottom-color: var(--dropdown-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
color: var(--dropdown-text);
|
||||
background-color: var(--dropdown-bg);
|
||||
border: 1px solid var(--dropdown-border);
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
|
||||
LI {
|
||||
padding: 10px 50px 10px 20px;
|
||||
|
||||
&.divider {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
> .divider-inner {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--dropdown-divider);
|
||||
width: 125%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.divider):hover {
|
||||
background-color: var(--dropdown-hover-bg);
|
||||
color: var(--dropdown-hover-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//header
|
||||
.user-info {
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@
|
|||
--body-gradient: linear-gradient(180deg, #{$dark} 0%, #{$darkest} 100%);
|
||||
--body-text: #{$lightest};
|
||||
--scrollbar-thumb: #{$medium};
|
||||
--scrollbar-thumb-dropdown: #{$darker};
|
||||
--scrollbar-thumb-dropdown: #{$medium};
|
||||
|
||||
--nav-bg: #{$darkest};
|
||||
--nav-active: #{rgba($primary, 0.3)};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.v-select,
|
||||
.v-select * {
|
||||
box-sizing: border-box;
|
||||
|
|
@ -23,7 +22,6 @@
|
|||
.vs__dropdown-toggle,
|
||||
.vs__clear,
|
||||
.vs__search,
|
||||
// .vs__selected,
|
||||
.vs__open-indicator {
|
||||
cursor: not-allowed;
|
||||
color: var(--dropdown-disabled-text);
|
||||
|
|
@ -33,24 +31,19 @@
|
|||
.vs__dropdown-menu {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: calc(100% - 1px);
|
||||
left: 0;
|
||||
left: -2px;
|
||||
z-index: z-index('dropdownContent');
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
width: calc(100% + 4px);
|
||||
max-height: 350px;
|
||||
min-width: 160px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--dropdown-border);
|
||||
border-top-style: none;
|
||||
border-radius: var(--border-radius);
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background: var(--dropdown-bg);
|
||||
box-shadow: 0px 8px 16px 0px var(--shadow);
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-thumb-dropdown) !important;
|
||||
|
|
@ -60,7 +53,6 @@
|
|||
&[data-popper-placement='top'] {
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-top-style: solid;
|
||||
border-bottom-style: none;
|
||||
box-shadow: 0px -8px 16px 0px var(--shadow);
|
||||
}
|
||||
}
|
||||
|
|
@ -89,8 +81,8 @@
|
|||
color: var(--dropdown-disabled-text);
|
||||
cursor: not-allowed;
|
||||
|
||||
HR {
|
||||
cursor: default
|
||||
hr {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +96,7 @@
|
|||
color: var(--dropdown-hover-text);
|
||||
background: var(--dropdown-hover-bg);
|
||||
|
||||
A {
|
||||
a {
|
||||
color: var(--dropdown-hover-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
@ -114,7 +106,6 @@
|
|||
.vs__dropdown-toggle {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
// padding: 0 0 4px 0;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--dropdown-border);
|
||||
border-radius: var(--border-radius);
|
||||
|
|
@ -139,7 +130,9 @@
|
|||
align-items: center;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
left: -2px;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 8;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
|
|
@ -161,32 +154,17 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vs--unsearchable .vs__search {
|
||||
background-color: var(--default-text);
|
||||
width: 0px;
|
||||
padding: 0;
|
||||
align-self: center;
|
||||
border: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vs--open .vs__dropdown-toggle {
|
||||
border-bottom-color: transparent;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
$transition-timing-function: cubic-bezier(1.000, -0.115, 0.975, 0.855);
|
||||
$transition-timing-function: cubic-bezier(1, -0.115, 0.975, 0.855);
|
||||
$transition-duration: 150ms;
|
||||
|
||||
.vs__open-indicator {
|
||||
.vs__action:after {
|
||||
fill: var(--dropdown-disabled-text);
|
||||
transform: scale(1);
|
||||
transition: transform $transition-duration $transition-timing-function;
|
||||
transition-timing-function: $transition-timing-function;
|
||||
}
|
||||
|
||||
.vs--open .vs__open-indicator {
|
||||
.vs--open .vs__actions:after {
|
||||
transform: rotate(180deg) scale(1);
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +177,7 @@ $transition-duration: 150ms;
|
|||
* below, the cancel button will still appear in chrome.
|
||||
* If it's up here on it's own, it'll hide it.
|
||||
*/
|
||||
.vs__search::-webkit-search-cancel-button {
|
||||
.vs__search::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -238,11 +216,10 @@ $transition-duration: 150ms;
|
|||
}
|
||||
.vs--single.vs--searching:not(.vs--open):not(.vs--loading) {
|
||||
.vs__search {
|
||||
opacity: .2;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* States */
|
||||
|
||||
.vs--single {
|
||||
|
|
@ -263,7 +240,7 @@ $transition-duration: 150ms;
|
|||
border: 1px solid var(--primary);
|
||||
border-radius: 3px;
|
||||
color: var(--link-text);
|
||||
padding: 6px;
|
||||
margin-left: 7px;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 2px;
|
||||
|
|
@ -296,47 +273,49 @@ $transition-duration: 150ms;
|
|||
.v-select.inline {
|
||||
background-color: transparent;
|
||||
|
||||
&.vs--single .vs__selected {
|
||||
color: var(--input-text);
|
||||
&.vs--single {
|
||||
min-height: 25px;
|
||||
&.vs--open {
|
||||
.vs__selected {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom:0;
|
||||
align-self: start;
|
||||
margin: 4px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.vs__search {
|
||||
margin-left: 7px;;
|
||||
}
|
||||
}
|
||||
.vs__selected {
|
||||
color: var(--input-text);
|
||||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-menu {
|
||||
min-width: 0px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.vs__dropdown-toggle {
|
||||
background-color: var(--input-bg);
|
||||
border:none;
|
||||
height: 100%;
|
||||
border: none;
|
||||
padding: none;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--dropdown-border);
|
||||
}
|
||||
|
||||
&.vs--single .vs__selected-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.vs__search {
|
||||
background-color: rgba(0,0,0,0);
|
||||
width: 100%;
|
||||
&:hover{
|
||||
background-color: rgba(0,0,0,0);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.vs__open-indicator{
|
||||
.vs__open-indicator {
|
||||
fill: var(--input-label);
|
||||
}
|
||||
.vs__clear {
|
||||
display:none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.v-select.mini {
|
||||
|
|
@ -351,15 +330,17 @@ $transition-duration: 150ms;
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
INPUT {
|
||||
input {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__selected-options INPUT {
|
||||
width: auto;
|
||||
.vs__selected-options input {
|
||||
width: 0;
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
background-color: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
header .vs-select .vs__dropdown-toggle {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ assignTo:
|
|||
=1 { Assign Cluster To… }
|
||||
other { Assign {count} Clusters To… }
|
||||
}
|
||||
workspace: Workspace
|
||||
|
||||
asyncButton:
|
||||
default:
|
||||
|
|
@ -327,21 +328,41 @@ chartHeading:
|
|||
|
||||
cis:
|
||||
addTest: Add Test ID
|
||||
alertNeeded: To receive alerts, please ensure <a tabindex="0" aria-label="Link to Rancher's Monitoring" href="{link}"> Rancher's Monitoring and Alerting app</a> is installed and the Receivers and Routes are <a target="_blank" rel='noopener nofollow' href='https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/configuration/#alertmanager-config'> configured to send out alerts.</a>
|
||||
alertNotFound: "It looks like alerting isn't configured."
|
||||
alertOnComplete: Alert on scan completion
|
||||
alertOnFailure: Alert on scan failure
|
||||
benchmarkVersion: Benchmark Version
|
||||
cronSchedule:
|
||||
label: Schedule
|
||||
placeholder: "e.g. 0 * * * *"
|
||||
deleteProfileWarning: |-
|
||||
{count, plural,
|
||||
=1 { Any scheduled scans using this profile will no longer work. }
|
||||
other { Any scheduled scans using either of these profiles will no longer work. }
|
||||
}
|
||||
downloadAllReports: Download All Saved Reports
|
||||
downloadLatestReport: Download Latest Report
|
||||
noProfiles: There are no valid ClusterScanProfiles for this cluster type to select.
|
||||
profile: Profile
|
||||
retention: Retention Count
|
||||
testID: Test ID
|
||||
testsToSkip: Tests to Skip
|
||||
testsSkipped: Tests Skipped
|
||||
scan:
|
||||
description: Description
|
||||
failed: Failed
|
||||
fail: Fail
|
||||
lastScanTime: Last Scan Time
|
||||
notApplicable: 'N/A'
|
||||
number: Number
|
||||
passed: Passed
|
||||
pass: Pass
|
||||
scanReport: Scan Report
|
||||
skipped: Skipped
|
||||
skip: Skip
|
||||
total: Total
|
||||
warn: Warn
|
||||
scoreWarning:
|
||||
label: Scan state for "warn" results
|
||||
protip: Scans with no failures will be marked "Pass" by default even if some of the tests generate "warn" output. This behavior can be changed by selecting the "fail" option from this section.
|
||||
|
||||
cluster:
|
||||
nodeDriver:
|
||||
|
|
@ -871,6 +892,24 @@ monitoring:
|
|||
fields:
|
||||
name: Name
|
||||
|
||||
monitoringRoute:
|
||||
groups:
|
||||
label: Group By
|
||||
info: This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted.
|
||||
interval:
|
||||
label: Group Interval
|
||||
matching:
|
||||
info: The root route has to match everything so matching can't be configured.
|
||||
label: Match
|
||||
receiver:
|
||||
label: Receiver
|
||||
regex:
|
||||
label: Match Regex
|
||||
repeatInterval:
|
||||
label: Repeat Interval
|
||||
wait:
|
||||
label: Group Wait
|
||||
|
||||
nameNsDescription:
|
||||
name:
|
||||
label: Name
|
||||
|
|
@ -887,6 +926,16 @@ nameNsDescription:
|
|||
|
||||
namespace:
|
||||
containerResourceLimit: Container Resource Limit
|
||||
project:
|
||||
label: Project
|
||||
|
||||
namespaceFilter:
|
||||
selected:
|
||||
label: "{total} items selected"
|
||||
|
||||
namespaceList:
|
||||
selectLabel: Namespace
|
||||
addLabel: Add Namespace
|
||||
|
||||
node:
|
||||
detail:
|
||||
|
|
@ -957,6 +1006,39 @@ prefs:
|
|||
hideDesc:
|
||||
label: Hide All Type Description Boxes
|
||||
|
||||
probe:
|
||||
checkInterval:
|
||||
label: Check Interval
|
||||
placeholder: 'Default: 10'
|
||||
command:
|
||||
label: Command to run
|
||||
placeholder: e.g. cat /tmp/health
|
||||
failureThreshold:
|
||||
label: Failure Threshold
|
||||
placeholder: 'Default: 3'
|
||||
httpGet:
|
||||
headers:
|
||||
label: Request Headers
|
||||
path:
|
||||
label: Request Path
|
||||
placeholder: e.g. /healthz
|
||||
port:
|
||||
label: Check Port
|
||||
placeholder: e.g. 80
|
||||
placeholderDuex: e.g. 25
|
||||
initialDelay:
|
||||
label: Initial Delay
|
||||
placeholder: 'Default: 0'
|
||||
successThreshold:
|
||||
label: Success Threshold
|
||||
placeholder: 'Default: 1'
|
||||
timeout:
|
||||
label: Timeout
|
||||
placeholder: 'Default: 3'
|
||||
type:
|
||||
label: Type
|
||||
placeholder: Select a check type
|
||||
|
||||
prometheusRule:
|
||||
alertingRules:
|
||||
addLabel: Add Alert
|
||||
|
|
@ -1321,6 +1403,18 @@ tableHeaders:
|
|||
version: Version
|
||||
weight: Weight
|
||||
|
||||
target:
|
||||
router:
|
||||
label: Router
|
||||
placeholder: Select a router
|
||||
service:
|
||||
label: Service
|
||||
placeholder: Select a service
|
||||
title: Target
|
||||
version:
|
||||
label: Version
|
||||
placeholder: Select a version
|
||||
|
||||
validation:
|
||||
arrayLength:
|
||||
between: '"{key}" should contain between {min} and {max} {max, plural, =1 {item} other {items}}'
|
||||
|
|
@ -1489,6 +1583,7 @@ workload:
|
|||
value:
|
||||
label: Value
|
||||
placeholder: e.g. BAR
|
||||
tty: TTY
|
||||
workingDir: WorkingDir
|
||||
stdin: Stdin
|
||||
healthCheck:
|
||||
|
|
@ -1669,6 +1764,7 @@ workload:
|
|||
inNamespaces: "Pods in these namespaces:"
|
||||
key: Key
|
||||
lessThan: <
|
||||
namespaces: Namespaces
|
||||
notIn: ≠
|
||||
operator: Operator
|
||||
value: Value
|
||||
|
|
@ -1758,6 +1854,7 @@ workload:
|
|||
storagePolicyName: Storage Policy Name
|
||||
volumePath: Volume Path
|
||||
defaultMode: Default Mode
|
||||
driver: driver
|
||||
hostPath:
|
||||
label: The Path on the Node must be
|
||||
options:
|
||||
|
|
@ -2046,3 +2143,7 @@ workloadPorts:
|
|||
|
||||
podAffinity:
|
||||
addLabel: Add Pod Selector
|
||||
|
||||
keyValue:
|
||||
keyPlaceholder: e.g. foo
|
||||
valuePlaceholder: e.g. bar
|
||||
|
|
|
|||
|
|
@ -962,7 +962,7 @@ servicePorts:
|
|||
label: 协议
|
||||
target:
|
||||
label: 目标端口
|
||||
placeholder: 例如:80 or http
|
||||
placeholder: 例如:80 或 http
|
||||
|
||||
serviceTypes:
|
||||
clusterip: 集群IP地址
|
||||
|
|
@ -1130,6 +1130,23 @@ tableHeaders:
|
|||
value: 值
|
||||
version: 版本号
|
||||
weight: 权重 ## doublecheck,这里的权重有什么特殊意义吗?
|
||||
Capacity: 容量
|
||||
'Access Modes': 访问模式
|
||||
'Reclaim Policy': 重声明策略
|
||||
Status: 状态
|
||||
Claim: 声明
|
||||
StorageClass: 存储类
|
||||
Reason: 原因
|
||||
VolumeMode: 存储卷模式
|
||||
Targets: 目标
|
||||
MinPods: 最小Pod数量
|
||||
MaxPods: 最大Pod数量
|
||||
Replicas: 副本数
|
||||
Provisioner: 提供商
|
||||
ReclaimPolicy: 重声明策略
|
||||
Role: 角色
|
||||
Users: 用户
|
||||
Groups: 用户组
|
||||
|
||||
validation:
|
||||
arrayLength:
|
||||
|
|
@ -1659,6 +1676,16 @@ typeLabel:
|
|||
persistentvolume: 持久卷
|
||||
service: 服务
|
||||
node: 节点
|
||||
autoscaling.horizontalpodautoscaler: Pod水平自动伸缩
|
||||
networking.k8s.io.ingress: Ingress
|
||||
networking.k8s.io.networkpolicy: 网络策略
|
||||
persistentvolumeclaim: 持久卷声明
|
||||
storage.k8s.io.storageclass: 存储类
|
||||
secret: 密钥
|
||||
rbac.authorization.k8s.io.clusterrolebinding: 集群角色绑定
|
||||
rbac.authorization.k8s.io.clusterrole: 集群角色
|
||||
rbac.authorization.k8s.io.rolebinding: 角色绑定
|
||||
rbac.authorization.k8s.io.role: 角色
|
||||
|
||||
action:
|
||||
download: 下载YAML
|
||||
|
|
@ -1684,3 +1711,7 @@ workloadPorts:
|
|||
|
||||
podAffinity:
|
||||
addLabel: 添加 Pod Selector
|
||||
|
||||
keyValue:
|
||||
keyPlaceholder: '例如: foo'
|
||||
valuePlaceholder: '例如: bar'
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default {
|
|||
<form>
|
||||
<LabeledSelect
|
||||
v-model="moveTo"
|
||||
label="Workspace"
|
||||
:label="t('assignTo.workspace')"
|
||||
:options="workspaceOptions"
|
||||
placement="bottom"
|
||||
/>
|
||||
|
|
@ -141,7 +141,7 @@ export default {
|
|||
|
||||
<div slot="actions">
|
||||
<button class="btn role-secondary" @click="close">
|
||||
Cancel
|
||||
{{ t('generic.cancel') }}
|
||||
</button>
|
||||
|
||||
<AsyncButton
|
||||
|
|
|
|||
|
|
@ -1,101 +1,235 @@
|
|||
<script>
|
||||
import { get } from '@/utils/object';
|
||||
import isString from 'lodash/isString';
|
||||
import VueSelectOverrides from '@/mixins/vue-select-overrides';
|
||||
|
||||
export default {
|
||||
mixins: [VueSelectOverrides],
|
||||
props: {
|
||||
size: {
|
||||
buttonLabel: {
|
||||
default: '',
|
||||
type: String,
|
||||
default: '' // possible values are xs, sm, lg. empty is default .btn
|
||||
},
|
||||
// whether this is a button and dropdown (default) or dropdown that looks like a button/dropdown
|
||||
dualAction: {
|
||||
closeOnSelect: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttonSize() {
|
||||
const { size } = this;
|
||||
let out;
|
||||
|
||||
switch (size) {
|
||||
case '':
|
||||
default:
|
||||
out = 'btn';
|
||||
break;
|
||||
case 'xs':
|
||||
out = 'btn btn-xs';
|
||||
break;
|
||||
case 'sm':
|
||||
out = 'btn btn-sm';
|
||||
break;
|
||||
case 'lg':
|
||||
out = 'btn btn-lg';
|
||||
break;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
// array of option objects containing at least a label and link, but also icon and action are available
|
||||
dropdownOptions: {
|
||||
// required: true,
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
optionKey: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
optionLabel: {
|
||||
default: 'label',
|
||||
type: String,
|
||||
},
|
||||
// sm, null(med), lg - no xs...its so small
|
||||
size: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { focused: false };
|
||||
},
|
||||
|
||||
methods: {
|
||||
hasSlot(name = 'default') {
|
||||
return !!this.$slots[name] || !!this.$scopedSlots[name];
|
||||
ddButtonAction(option) {
|
||||
this.focusSearch();
|
||||
this.$emit('dd-button-action', option);
|
||||
},
|
||||
getOptionLabel(option) {
|
||||
if (isString(option)) {
|
||||
return option;
|
||||
}
|
||||
|
||||
if (this.$attrs['get-option-label']) {
|
||||
return this.$attrs['get-option-label'](option);
|
||||
}
|
||||
|
||||
if (get(option, this.optionLabel)) {
|
||||
if (this.localizedLabel) {
|
||||
return this.$store.getters['i18n/t'](get(option, this.optionLabel));
|
||||
} else {
|
||||
return get(option, this.optionLabel);
|
||||
}
|
||||
} else {
|
||||
return option;
|
||||
}
|
||||
},
|
||||
|
||||
// allows parent components to programmatically open the dropdown
|
||||
togglePopover() {
|
||||
this.$refs.popoverButton.click();
|
||||
onFocus() {
|
||||
return this.onFocusLabeled();
|
||||
},
|
||||
|
||||
onFocusLabeled() {
|
||||
this.focused = true;
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
return this.onBlurLabeled();
|
||||
},
|
||||
|
||||
onBlurLabeled() {
|
||||
this.focused = false;
|
||||
},
|
||||
|
||||
focusSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs['button-dropdown'].searchEl.focus();
|
||||
});
|
||||
},
|
||||
get,
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-button-group">
|
||||
<div
|
||||
class="dropdown-button bg-primary"
|
||||
:class="{'one-action':!dualAction, [buttonSize]:true}"
|
||||
<v-select
|
||||
ref="button-dropdown"
|
||||
class="button-dropdown btn"
|
||||
:class="{
|
||||
disabled,
|
||||
focused,
|
||||
'btn-sm': size === 'sm',
|
||||
'btn-lg': size === 'lg',
|
||||
}"
|
||||
v-bind="$attrs"
|
||||
:searchable="false"
|
||||
:clearable="false"
|
||||
:close-on-select="closeOnSelect"
|
||||
:filterable="false"
|
||||
:value="buttonLabel"
|
||||
:options="dropdownOptions"
|
||||
:map-keydown="mappedKeys"
|
||||
:get-option-key="
|
||||
(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))
|
||||
"
|
||||
:get-option-label="(opt) => getOptionLabel(opt)"
|
||||
@search:blur="onBlur"
|
||||
@search:focus="onFocus"
|
||||
@input="$emit('click-action', $event)"
|
||||
>
|
||||
<slot v-if="dualAction" name="button-content" :buttonSize="buttonSize">
|
||||
<template #selected-option="option">
|
||||
<button
|
||||
class="bg-transparent"
|
||||
:class="buttonSize"
|
||||
disabled="true"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
class="dropdown-button-two btn"
|
||||
:class="{
|
||||
'btn-sm': size === 'sm',
|
||||
'btn-lg': size === 'lg',
|
||||
}"
|
||||
@click="ddButtonAction(option)"
|
||||
@focus="focusSearch"
|
||||
>
|
||||
Button
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</slot>
|
||||
<div
|
||||
v-else
|
||||
:class="buttonSize"
|
||||
type="button"
|
||||
>
|
||||
<slot name="button-content" />
|
||||
</div>
|
||||
<div v-if="hasSlot('popover-content') && dualAction" class="button-divider"></div>
|
||||
|
||||
<v-popover
|
||||
v-if="hasSlot('popover-content')"
|
||||
placement="bottom"
|
||||
:container="false"
|
||||
offset="10"
|
||||
:popper-options="{modifiers: { flip: { enabled: false } } }"
|
||||
>
|
||||
<slot name="button-toggle-content" :buttonSize="buttonSize">
|
||||
<button
|
||||
ref="popoverButton"
|
||||
class="icon-container bg-transparent"
|
||||
:class="buttonSize"
|
||||
type="button"
|
||||
>
|
||||
<i class="icon icon-chevron-down" />
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<template slot="popover">
|
||||
<slot name="popover-content" />
|
||||
</template>
|
||||
</v-popover>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pass down templates provided by the caller -->
|
||||
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
|
||||
<slot v-if="slot !== 'selected-option'" :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</v-select>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.button-dropdown.btn-sm {
|
||||
::v-deep > .vs__dropdown-toggle {
|
||||
.vs__actions {
|
||||
&:after {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.button-dropdown.btn-lg {
|
||||
::v-deep > .vs__dropdown-toggle {
|
||||
.vs__actions {
|
||||
&:after {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.button-dropdown {
|
||||
background: var(--tooltip-bg);
|
||||
border: solid 1px var(--link-text);
|
||||
color: var(--link-text);
|
||||
padding: 0;
|
||||
|
||||
&.vs--open ::v-deep {
|
||||
outline: none;
|
||||
border: var(--outline-width) solid var(--outline);
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
::v-deep .vs__dropdown-toggle .vs__actions,
|
||||
::v-deep .vs__selected-options {
|
||||
background: var(--accent-btn-hover);
|
||||
}
|
||||
::v-deep .vs__selected-options .vs__selected button {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep > .vs__dropdown-toggle {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 75% 25%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
.vs__actions {
|
||||
border-left: solid thin var(--link-text);
|
||||
justify-content: center;
|
||||
|
||||
&:after {
|
||||
color: var(--link-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .vs__selected-options {
|
||||
.vs__selected {
|
||||
margin: unset;
|
||||
border: none;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: var(--tooltip-bg);
|
||||
color: var(--link-text);
|
||||
}
|
||||
}
|
||||
.vs__search {
|
||||
// if you need to keep the dd open you can toggle these on and off
|
||||
// display: none;
|
||||
// visibility: hidden;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .vs__dropdown-menu {
|
||||
min-width: unset;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@
|
|||
import { mapState, mapGetters } from 'vuex';
|
||||
import { get, isEmpty } from '@/utils/object';
|
||||
import { NAMESPACE, RIO } from '@/config/types';
|
||||
import Card from '@/components/Card';
|
||||
import InfoBox from '@/components/InfoBox';
|
||||
import { alternateLabel } from '@/utils/platform';
|
||||
import LinkDetail from '@/components/formatter/LinkDetail';
|
||||
import { uniq } from '@/utils/array';
|
||||
|
||||
export default {
|
||||
components: { Card, LinkDetail },
|
||||
components: { InfoBox, LinkDetail },
|
||||
data() {
|
||||
return { confirmName: '', error: '' };
|
||||
return {
|
||||
confirmName: '', error: '', warning: '', preventDelete: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
names() {
|
||||
|
|
@ -54,20 +56,6 @@ export default {
|
|||
return (type === NAMESPACE || type === RIO.STACK) && this.toRemove.length === 1;
|
||||
},
|
||||
|
||||
preventDeletionMessage() {
|
||||
const toRemoveWithWarning = this.toRemove.filter(tr => tr?.preventDeletionMessage);
|
||||
|
||||
if (toRemoveWithWarning.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toRemoveWithWarning[0].preventDeletionMessage;
|
||||
},
|
||||
|
||||
isDeleteDisabled() {
|
||||
return !!this.preventDeletionMessage;
|
||||
},
|
||||
|
||||
plusMore() {
|
||||
const remaining = this.toRemove.length - this.names.length;
|
||||
|
||||
|
|
@ -126,6 +114,31 @@ export default {
|
|||
} else {
|
||||
this.$modal.hide('promptRemove');
|
||||
}
|
||||
},
|
||||
|
||||
// check for any resources with a deletion prevention message,
|
||||
// if none found (delete is allowed), then check for any resources with a warning message
|
||||
toRemove(neu) {
|
||||
let message;
|
||||
const preventDeletionMessages = neu.filter(item => item.preventDeletionMessage);
|
||||
|
||||
if (!!preventDeletionMessages.length) {
|
||||
this.preventDelete = true;
|
||||
message = preventDeletionMessages[0].preventDeletionMessage;
|
||||
} else {
|
||||
const warnDeletionMessages = neu.filter(item => item.warnDeletionMessage);
|
||||
|
||||
if (!!warnDeletionMessages.length) {
|
||||
message = warnDeletionMessages[0].warnDeletionMessage;
|
||||
}
|
||||
}
|
||||
if (typeof message === 'function' ) {
|
||||
this.warning = message(this.toRemove);
|
||||
} else if (!!message) {
|
||||
this.warning = message;
|
||||
} else {
|
||||
this.warning = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -219,7 +232,7 @@ export default {
|
|||
height="auto"
|
||||
styles="background-color: var(--nav-bg); border-radius: var(--border-radius); max-height: 100vh;"
|
||||
>
|
||||
<Card>
|
||||
<InfoBox>
|
||||
<h4 slot="title" class="text-default-text">
|
||||
Are you sure?
|
||||
</h4>
|
||||
|
|
@ -231,10 +244,13 @@ export default {
|
|||
<span v-if="i===names.length-1" :key="resource+2">{{ plusMore }}</span><span v-else :key="resource+1">{{ i === toRemove.length-2 ? ', and ' : ', ' }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<span class="text-warning">
|
||||
{{ warning }}
|
||||
</span>
|
||||
<span v-if="needsConfirm" :key="resource">Re-enter its name below to confirm:</span>
|
||||
</div>
|
||||
<input v-if="needsConfirm" id="confirm" v-model="confirmName" type="text" />
|
||||
<span class="text-warning">{{ preventDeletionMessage }}</span>
|
||||
|
||||
<span class="text-error">{{ error }}</span>
|
||||
<span v-if="!needsConfirm" class="text-info mt-20">{{ protip }}</span>
|
||||
</div>
|
||||
|
|
@ -242,11 +258,11 @@ export default {
|
|||
<button class="btn role-secondary" @click="close">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn bg-error" :disabled="isDeleteDisabled" @click="remove">
|
||||
<button class="btn bg-error" :disabled="preventDelete" @click="remove">
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
</Card>
|
||||
</InfoBox>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Favorite from '@/components/nav/Favorite';
|
||||
import ButtonDropdown from '@/components/ButtonDropdown';
|
||||
import TypeDescription from '@/components/TypeDescription';
|
||||
import { get } from '@/utils/object';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ButtonDropdown,
|
||||
Favorite,
|
||||
TypeDescription,
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
schema: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
typeDisplay: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
isCreatable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
isYamlCreatable: {
|
||||
type: Boolean,
|
||||
|
|
@ -33,19 +32,20 @@ export default {
|
|||
},
|
||||
createLocation: {
|
||||
type: Object,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
yamlCreateLocation: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
get,
|
||||
...mapGetters(['isExplorer']),
|
||||
|
||||
resourceName() {
|
||||
if ( this.schema ) {
|
||||
if (this.schema) {
|
||||
return this.$store.getters['type-map/labelFor'](this.schema);
|
||||
}
|
||||
|
||||
|
|
@ -62,64 +62,26 @@ export default {
|
|||
{{ typeDisplay }} <Favorite v-if="isExplorer" :resource="resource" />
|
||||
</h1>
|
||||
<div class="actions">
|
||||
<ButtonDropdown
|
||||
v-if="isCreatable || isYamlCreatable"
|
||||
>
|
||||
<template #button-content="slotProps">
|
||||
<nuxt-link
|
||||
v-if="isCreatable"
|
||||
:to="createLocation"
|
||||
class="btn bg-transparent"
|
||||
:class="slotProps.buttonSize"
|
||||
class="btn role-primary"
|
||||
>
|
||||
{{ t("resourceList.head.create") }}
|
||||
{{ t('resourceList.head.create') }}
|
||||
</nuxt-link>
|
||||
<nuxt-link
|
||||
v-else-if="!isCreatable && isYamlCreatable"
|
||||
:to="yamlCreateLocation"
|
||||
class="btn bg-transparent"
|
||||
:class="slotProps.buttonSize"
|
||||
>
|
||||
{{ t("resourceList.head.createFromYaml") }}
|
||||
</nuxt-link>
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="btn bg-transparent"
|
||||
:class="slotProps.buttonSize"
|
||||
disabled="true"
|
||||
@click.prevent.self
|
||||
>
|
||||
{{ t("resourceList.head.create") }}
|
||||
</a>
|
||||
</template>
|
||||
<template
|
||||
v-if="isCreatable && isYamlCreatable"
|
||||
slot="popover-content"
|
||||
>
|
||||
<ul class="list-unstyled menu" style="margin: -1px;">
|
||||
<li class="hand">
|
||||
<nuxt-link
|
||||
v-if="isCreatable"
|
||||
:to="createLocation"
|
||||
>
|
||||
{{ t("resourceList.head.createResource", { resourceName }) }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li class="divider">
|
||||
<div class="divider-inner"></div>
|
||||
</li>
|
||||
<li class="hand">
|
||||
<nuxt-link
|
||||
v-if="isYamlCreatable"
|
||||
:to="yamlCreateLocation"
|
||||
class="btn role-primary"
|
||||
>
|
||||
{{ t("resourceList.head.createFromYaml") }}
|
||||
{{ t('resourceList.head.createFromYaml') }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ButtonDropdown>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.actions {
|
||||
grid-column: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,9 @@ export default {
|
|||
|
||||
addLabel: {
|
||||
type: String,
|
||||
default: 'Add',
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('generic.add');
|
||||
},
|
||||
},
|
||||
addIcon: {
|
||||
type: String,
|
||||
|
|
@ -86,7 +88,9 @@ export default {
|
|||
|
||||
removeLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('generic.remove');
|
||||
},
|
||||
},
|
||||
removeIcon: {
|
||||
type: String,
|
||||
|
|
@ -295,7 +299,6 @@ export default {
|
|||
<div v-if="showRemove" class="remove">
|
||||
<slot name="remove-button" :remove="() => remove(idx)">
|
||||
<button type="button" class="btn role-link" @click="remove(idx)">
|
||||
Remove
|
||||
{{ removeLabel }}
|
||||
</button>
|
||||
</slot>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default {
|
|||
ShellInput,
|
||||
LabeledSelect,
|
||||
Checkbox,
|
||||
EnvVars
|
||||
EnvVars,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -22,29 +22,39 @@ export default {
|
|||
},
|
||||
configMaps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
|
||||
default: () => [],
|
||||
},
|
||||
secrets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
// container spec
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const {
|
||||
command, args, workingDir, stdin = false, stdinOnce = false, tty = false
|
||||
command,
|
||||
args,
|
||||
workingDir,
|
||||
stdin = false,
|
||||
stdinOnce = false,
|
||||
tty = false,
|
||||
} = this.value;
|
||||
|
||||
return {
|
||||
command, args, workingDir, stdin, stdinOnce, tty
|
||||
args,
|
||||
command,
|
||||
commandOptions: ['No', 'Once', 'Yes'],
|
||||
stdin,
|
||||
stdinOnce,
|
||||
tty,
|
||||
workingDir,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -78,8 +88,8 @@ export default {
|
|||
break;
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -93,10 +103,10 @@ export default {
|
|||
args: this.args,
|
||||
workingDir: this.workingDir,
|
||||
tty: this.tty,
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
this.$emit('input', out );
|
||||
this.$emit('input', out);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -136,18 +146,33 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<div :style="{'align-items':'center'}" class="row">
|
||||
<div :style="{ 'align-items': 'center' }" class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect v-model="stdinSelect" :label="t('workload.container.command.stdin')" :options="['No', 'Once', 'Yes']" :mode="mode" />
|
||||
<LabeledSelect
|
||||
v-model="stdinSelect"
|
||||
:label="t('workload.container.command.stdin')"
|
||||
:options="commandOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="stdin" class="col span-6">
|
||||
<Checkbox v-model="tty" :mode="mode" label="TTY" @input="update" />
|
||||
<Checkbox
|
||||
v-model="tty"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.command.stdin')"
|
||||
@input="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<h3>{{ t('workload.container.titles.env') }}</h3>
|
||||
<EnvVars :mode="mode" :config-maps="configMaps" :secrets="secrets" :value="value" />
|
||||
<EnvVars
|
||||
:mode="mode"
|
||||
:config-maps="configMaps"
|
||||
:secrets="secrets"
|
||||
:value="value"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -112,8 +112,16 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
<style lang='scss' scoped>
|
||||
.value-from ::v-deep {
|
||||
.v-select {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
INPUT:not(.vs__search) {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
.value-from, .value-from-headers {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20% 20% 5% 20% auto;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { mapState } from 'vuex';
|
|||
import debounce from 'lodash/debounce';
|
||||
import { findBy } from '@/utils/array';
|
||||
import { EXTENDED_SCOPES } from '@/store/github';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
|
||||
export const FILE_PATTERNS = {
|
||||
dockerfile: /^Dockerfile(\..*)?$/i,
|
||||
|
|
@ -11,6 +12,7 @@ export const FILE_PATTERNS = {
|
|||
};
|
||||
|
||||
export default {
|
||||
components: { LabeledSelect },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
|
|
@ -280,7 +282,7 @@ export default {
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-4">
|
||||
<v-select
|
||||
<LabeledSelect
|
||||
:placeholder="repoPlaceholder"
|
||||
:disabled="loadingRecentRepos"
|
||||
:options="repos"
|
||||
|
|
@ -310,10 +312,10 @@ export default {
|
|||
{{ option.full_name }}
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<v-select
|
||||
<LabeledSelect
|
||||
:disabled="!selectedRepo || loadingBranches"
|
||||
:placeholder="branchPlaceholder"
|
||||
:options="branches"
|
||||
|
|
@ -322,10 +324,10 @@ export default {
|
|||
:clearable="false"
|
||||
@input="selectBranch"
|
||||
>
|
||||
</v-select>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<v-select
|
||||
<LabeledSelect
|
||||
:disabled="!selectedBranch"
|
||||
:placeholder="filePlaceholder"
|
||||
:options="files"
|
||||
|
|
@ -334,7 +336,7 @@ export default {
|
|||
:clearable="false"
|
||||
@input="selectFile"
|
||||
>
|
||||
</v-select>
|
||||
</LabeledSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
import labeledFormElement from '@/mixins/labeled-form-element';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
import UnitInput from '@/components/form/UnitInput';
|
||||
export default {
|
||||
components: {
|
||||
LabeledInput, LabeledSelect, UnitInput
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
UnitInput,
|
||||
Select,
|
||||
},
|
||||
mixins: [labeledFormElement],
|
||||
props: {
|
||||
|
|
@ -26,22 +30,22 @@ export default {
|
|||
|
||||
selectLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
|
||||
selectValue: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
|
||||
optionLabel: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
default: 'label',
|
||||
},
|
||||
|
||||
options: {
|
||||
type: Array,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
|
||||
selectBeforeText: {
|
||||
|
|
@ -51,34 +55,37 @@ export default {
|
|||
|
||||
textLabel: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
|
||||
textRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
|
||||
textValue: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return { selected: this.selectValue || this.options[0], string: this.textValue };
|
||||
return {
|
||||
selected: this.selectValue || this.options[0],
|
||||
string: this.textValue,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
const comp = this.$refs.text;
|
||||
|
||||
if ( comp ) {
|
||||
if (comp) {
|
||||
comp.focus();
|
||||
}
|
||||
},
|
||||
|
|
@ -86,29 +93,57 @@ export default {
|
|||
change() {
|
||||
this.$emit('input', { selected: this.selected, text: this.string });
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isView && !selectBeforeText">
|
||||
<UnitInput mode="view" :value="string" :label="textLabel" :suffix="selected" />
|
||||
<UnitInput
|
||||
mode="view"
|
||||
:value="string"
|
||||
:label="textLabel"
|
||||
:suffix="selected"
|
||||
/>
|
||||
</div>
|
||||
<div v-else :class="{'select-after':!selectBeforeText}" class="input-container row" @input="change">
|
||||
<div
|
||||
v-else
|
||||
:class="{ 'select-after': !selectBeforeText }"
|
||||
class="input-container row"
|
||||
@input="change"
|
||||
>
|
||||
<LabeledSelect
|
||||
v-if="selectLabel"
|
||||
v-model="selected"
|
||||
:label="selectLabel"
|
||||
:class="{'in-input': !isView}"
|
||||
:class="{ 'in-input': !isView }"
|
||||
:options="options"
|
||||
:searchable="searchable"
|
||||
:disbaled="isView"
|
||||
:searchable="false"
|
||||
:clearable="false"
|
||||
:disabled="disabled"
|
||||
:disabled="disabled || isView"
|
||||
:taggable="taggable"
|
||||
:create-option="name => ({ label: name, value: name })"
|
||||
:create-option="(name) => ({ label: name, value: name })"
|
||||
:multiple="false"
|
||||
:mode="mode"
|
||||
:option-label="optionLabel"
|
||||
:placement="$attrs.placement ? $attrs.placement : null"
|
||||
:v-bind="$attrs"
|
||||
@input="change"
|
||||
/>
|
||||
<Select
|
||||
v-else
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
:searchable="searchable"
|
||||
:disabled="disabled || isView"
|
||||
:clearable="false"
|
||||
:class="{ 'in-input': !isView }"
|
||||
:taggable="taggable"
|
||||
:create-option="(name) => ({ label: name, value: name })"
|
||||
:multiple="false"
|
||||
:mode="mode"
|
||||
:option-label="optionLabel"
|
||||
:placement="$attrs.placement ? $attrs.placement : null"
|
||||
:v-bind="$attrs"
|
||||
@input="change"
|
||||
/>
|
||||
|
|
@ -143,8 +178,8 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
.input-container {
|
||||
<style lang='scss' scoped>
|
||||
.input-container {
|
||||
display: flex;
|
||||
height: 52px;
|
||||
|
||||
|
|
@ -163,21 +198,20 @@ export default {
|
|||
border-right: 1px solid var(--border);
|
||||
|
||||
&.labeled-select {
|
||||
width: 20%;
|
||||
|
||||
.selected{
|
||||
.selected {
|
||||
color: var(--input-text);
|
||||
text-align: center;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
& .input-string {
|
||||
padding-right: 8px;
|
||||
padding-right: 0;
|
||||
height: 100%;
|
||||
width:60%;
|
||||
width: 60%;
|
||||
flex-grow: 1;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
border-left: 0;
|
||||
|
|
@ -185,30 +219,30 @@ export default {
|
|||
display: initial;
|
||||
}
|
||||
|
||||
.in-input {
|
||||
& .in-input {
|
||||
margin-right: 0;
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
|
||||
&.labeled-select {
|
||||
display: block;
|
||||
&.labeled-select.focused ::v-deep,
|
||||
&.unlabeled-select.focused ::v-deep {
|
||||
outline: none;
|
||||
border: var(--outline-width) solid var(--outline);
|
||||
}
|
||||
|
||||
&.labeled-select ::v-deep,
|
||||
&.unlabeled-select ::v-deep {
|
||||
box-shadow: none;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
background-color: var(--accent-btn);
|
||||
border: solid 1px var(--primary);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: solid 1px var(--primary);
|
||||
|
||||
.vs__selected {
|
||||
margin: 0;
|
||||
color: var(--input-text)
|
||||
color: var(--input-text);
|
||||
}
|
||||
|
||||
.vs__dropdown-menu {
|
||||
width: calc(100% + 2px);
|
||||
left: -1px;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--primary);
|
||||
.vs__dropdown-option {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
|
@ -217,38 +251,9 @@ export default {
|
|||
.vs__dropdown-toggle {
|
||||
color: var(--primary) !important;
|
||||
height: 100%;
|
||||
padding: none;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
& * {
|
||||
padding: 0
|
||||
}
|
||||
}
|
||||
|
||||
.vs__selected-options {
|
||||
display: -webkit-box;
|
||||
& .labeled-input {
|
||||
top:10px;
|
||||
& LABEL, .selected {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vs__actions {
|
||||
padding: 2px;;
|
||||
}
|
||||
|
||||
.vs__open-indicator {
|
||||
fill: var(--primary);
|
||||
transform: scale(0.75);
|
||||
}
|
||||
|
||||
&.vs--open .vs__open-indicator {
|
||||
transform: rotate(180deg) scale(0.75);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import ClickExpand from '@/components/formatter/ClickExpand';
|
|||
import { get } from '@/utils/object';
|
||||
import CodeMirror from '@/components/CodeMirror';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ButtonDropdown from '@/components/ButtonDropdown';
|
||||
import FileSelector from '@/components/form/FileSelector';
|
||||
import { HIDE_SENSITIVE } from '@/store/prefs';
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ export default {
|
|||
TextAreaAutoGrow,
|
||||
ClickExpand,
|
||||
CodeMirror,
|
||||
ButtonDropdown,
|
||||
FileSelector
|
||||
},
|
||||
|
||||
|
|
@ -75,7 +73,9 @@ export default {
|
|||
},
|
||||
keyPlaceholder: {
|
||||
type: String,
|
||||
default: 'e.g. foo'
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('keyValue.valuePlaceholder');
|
||||
},
|
||||
},
|
||||
|
||||
separatorLabel: {
|
||||
|
|
@ -96,7 +96,9 @@ export default {
|
|||
},
|
||||
valuePlaceholder: {
|
||||
type: String,
|
||||
default: 'e.g. bar'
|
||||
default() {
|
||||
return this.$store.getters['i18n/t']('keyValue.valuePlaceholder');
|
||||
},
|
||||
},
|
||||
valueCanBeEmpty: {
|
||||
type: Boolean,
|
||||
|
|
@ -524,21 +526,10 @@ export default {
|
|||
|
||||
<div v-if="!titleAdd && (showAdd || showRead)" class="footer">
|
||||
<slot name="add" :add="add">
|
||||
<ButtonDropdown size="sm">
|
||||
<template #button-content>
|
||||
<button v-if="showAdd" type="button" class="btn btn-sm add" @click="add()">
|
||||
<button v-if="showAdd" type="button" class="btn btn-sm role-secondary add" @click="add()">
|
||||
{{ addLabel }}
|
||||
</button>
|
||||
<FileSelector v-else class="btn-sm" :label="t('generic.readFromFile')" :include-file-name="true" @selected="onFileSelected" />
|
||||
</template>
|
||||
<template v-if="showRead && showAdd" #popover-content>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<FileSelector class="btn-sm role-link" :label="readLabel" :include-file-name="true" @selected="onFileSelected" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ButtonDropdown>
|
||||
<FileSelector v-if="showRead" class="btn-sm role-secondary" :label="t('generic.readFromFile')" :include-file-name="true" @selected="onFileSelected" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -566,7 +557,7 @@ export default {
|
|||
-ms-word-break: break-all;
|
||||
word-break: break-word;
|
||||
display:flex;
|
||||
align-items:start;
|
||||
align-items:flex-start;
|
||||
}
|
||||
|
||||
&.extra-column {
|
||||
|
|
@ -581,7 +572,7 @@ export default {
|
|||
width: 100%;
|
||||
margin: 10px 0px 10px 0px;
|
||||
&.key {
|
||||
align-self: start;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.text-monospace:not(.conceal) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import LabeledFormElement from '@/mixins/labeled-form-element';
|
|||
import { findBy } from '@/utils/array';
|
||||
import { get } from '@/utils/object';
|
||||
import LabeledTooltip from '@/components/form/LabeledTooltip';
|
||||
import VueSelectOverrides from '@/mixins/vue-select-overrides';
|
||||
|
||||
export default {
|
||||
components: { LabeledTooltip },
|
||||
mixins: [LabeledFormElement],
|
||||
mixins: [LabeledFormElement, VueSelectOverrides],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -24,15 +25,15 @@ export default {
|
|||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
optionKey: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
optionLabel: {
|
||||
type: String,
|
||||
default: 'label'
|
||||
default: 'label',
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
|
|
@ -40,30 +41,34 @@ export default {
|
|||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
hoverTooltip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
localizedLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
searchable: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
reduce: {
|
||||
type: Function,
|
||||
default: (e) => {
|
||||
if ( e && typeof e === 'object' && e.value !== undefined ) {
|
||||
if (e && typeof e === 'object' && e.value !== undefined) {
|
||||
return e.value;
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -74,15 +79,15 @@ export default {
|
|||
currentLabel() {
|
||||
let entry;
|
||||
|
||||
if ( this.grouped ) {
|
||||
for ( let i = 0 ; i < this.options.length && !entry ; i++ ) {
|
||||
if (this.grouped) {
|
||||
for (let i = 0; i < this.options.length && !entry; i++) {
|
||||
entry = findBy(this.options[i].items || [], 'value', this.value);
|
||||
}
|
||||
} else {
|
||||
entry = findBy(this.options || [], 'value', this.value);
|
||||
}
|
||||
|
||||
if ( entry ) {
|
||||
if (entry) {
|
||||
return entry.label;
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +96,11 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
focusSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.searchEl.focus();
|
||||
});
|
||||
},
|
||||
onFocus() {
|
||||
this.selectedVisibility = 'hidden';
|
||||
this.onFocusLabeled();
|
||||
|
|
@ -102,6 +112,9 @@ export default {
|
|||
},
|
||||
|
||||
getOptionLabel(option) {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
if (this.$attrs['get-option-label']) {
|
||||
return this.$attrs['get-option-label'](option);
|
||||
}
|
||||
|
|
@ -138,7 +151,7 @@ export default {
|
|||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: { offset: [0, -1] }
|
||||
options: { offset: [0, 2] },
|
||||
},
|
||||
{
|
||||
name: 'toggleClass',
|
||||
|
|
@ -147,7 +160,8 @@ export default {
|
|||
fn({ state }) {
|
||||
component.$el.setAttribute('x-placement', state.placement);
|
||||
},
|
||||
}]
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -163,26 +177,42 @@ export default {
|
|||
input.open = true;
|
||||
}
|
||||
},
|
||||
get
|
||||
get,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="labeled-select labeled-input" :class="{disabled: disabled && !isView, focused, [mode]: true, [status]: status, taggable: $attrs.taggable, hoverable: hoverTooltip }">
|
||||
<div :class="{'labeled-container': true, raised, empty, [mode]: true}" :style="{border:'none'}">
|
||||
<label v-if="label">
|
||||
<div
|
||||
class="labeled-select"
|
||||
:class="{
|
||||
disabled: disabled && !isView,
|
||||
focused,
|
||||
[mode]: true,
|
||||
[status]: status,
|
||||
taggable: $attrs.taggable,
|
||||
hoverable: hoverTooltip,
|
||||
}"
|
||||
@click="focusSearch"
|
||||
@focus="focusSearch"
|
||||
>
|
||||
<div
|
||||
:class="{ 'labeled-container': true, raised, empty, [mode]: true }"
|
||||
:style="{ border: 'none' }"
|
||||
>
|
||||
<label>
|
||||
{{ label }}
|
||||
<span v-if="required && !value" class="required">*</span>
|
||||
</label>
|
||||
<label v-if="label" class="corner">
|
||||
<slot name="corner" />
|
||||
</label>
|
||||
<div v-if="isView" :class="{'no-label':!(label||'').length}" class="selected">
|
||||
<span v-if="!currentLabel" class="text-muted">—</span>{{ currentLabel }}
|
||||
</div>
|
||||
<div v-else-if="!$attrs.multiple" :class="{'no-label':!(label||'').length}" class="selected" :style="{visibility:selectedVisibility}">
|
||||
<div
|
||||
v-if="isView"
|
||||
:class="{ 'no-label': !(label || '').length }"
|
||||
class="selected"
|
||||
>
|
||||
<span v-if="!currentLabel" class="text-muted">
|
||||
{{ currentLabel }}
|
||||
</span>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<v-select
|
||||
|
|
@ -192,123 +222,105 @@ export default {
|
|||
class="inline"
|
||||
:append-to-body="!!placement"
|
||||
:calculate-position="placement ? withPopper : undefined"
|
||||
:class="{'no-label':!(label||'').length}"
|
||||
:class="{ 'no-label': !(label || '').length, }"
|
||||
:disabled="isView || disabled"
|
||||
:get-option-key="opt=>optionKey ? get(opt, optionKey) : getOptionLabel(opt)"
|
||||
:get-option-label="opt=>getOptionLabel(opt)"
|
||||
:get-option-key="(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))"
|
||||
:get-option-label="(opt) => getOptionLabel(opt)"
|
||||
:label="optionLabel"
|
||||
:options="options"
|
||||
:map-keydown="mappedKeys"
|
||||
:placeholder="placeholder"
|
||||
:reduce="x => reduce(x)"
|
||||
:reduce="(x) => reduce(x)"
|
||||
:searchable="isSearchable"
|
||||
:value="value != null ? value : ''"
|
||||
@input="e=>$emit('input', e)"
|
||||
@input="(e) => $emit('input', e)"
|
||||
@search:blur="onBlur"
|
||||
@search:focus="onFocus"
|
||||
>
|
||||
<template v-if="!$attrs.multiple" v-slot:selected-option-container>
|
||||
<span style="display: none"></span>
|
||||
</template>
|
||||
|
||||
<!-- Pass down templates provided by the caller -->
|
||||
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</v-select>
|
||||
<LabeledTooltip v-if="tooltip && !focused" :hover="hoverTooltip" :value="tooltip" :status="status" />
|
||||
<LabeledTooltip
|
||||
v-if="tooltip && !focused"
|
||||
:hover="hoverTooltip"
|
||||
:value="tooltip"
|
||||
:status="status"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
<style lang='scss' scoped>
|
||||
.labeled-select {
|
||||
&.hoverable .v-select *{
|
||||
z-index: z-index('overContent')
|
||||
&.hoverable ::v-deep {
|
||||
.v-select * {
|
||||
z-index: z-index('overContent');
|
||||
}
|
||||
.labeled-container .selected {
|
||||
}
|
||||
|
||||
.labeled-container {
|
||||
padding: 8px 0 0 8px;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.view.labeled-input .labeled-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.labeled-container, .vs__dropdown-toggle, input, label {
|
||||
&.view {
|
||||
&.labeled-input {
|
||||
.labeled-container {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .vs__selected-options {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
::v-deep .v-select:not(.vs--single) {
|
||||
.vs__selected-options {
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .vs__actions {
|
||||
&:after {
|
||||
line-height: 1.85rem;
|
||||
position: relative;
|
||||
right: 3px;
|
||||
top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep &.disabled {
|
||||
.labeled-container,
|
||||
.vs__dropdown-toggle,
|
||||
input,
|
||||
label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
padding-top: 17px;
|
||||
}
|
||||
|
||||
.selected, .vs__selected-options {
|
||||
.vs__search, .vs__search:hover {
|
||||
background-color: transparent;
|
||||
padding: 2px 0 0 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.no-label {
|
||||
.no-label ::v-deep {
|
||||
&.v-select:not(.vs--single) {
|
||||
min-height: 33px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
padding-top:8px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 9px;
|
||||
position: relative;
|
||||
max-height:2.3em;
|
||||
overflow:hidden;
|
||||
max-height: 2.3em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vs__selected-options {
|
||||
padding:8px 0 7px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.focused .vs__dropdown-menu {
|
||||
outline: none;
|
||||
border: var(--outline-width) solid var(--outline);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.taggable {
|
||||
.vs__selected-options {
|
||||
margin: 14px 0px 2px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.v-select.inline {
|
||||
position: initial;
|
||||
|
||||
&.vs--single {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
.vs__search {
|
||||
background-color: transparent;
|
||||
padding: 18px 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&, .vs__dropdown-toggle, .vs__dropdown-toggle > * {
|
||||
background-color: transparent;
|
||||
border:transparent;
|
||||
}
|
||||
|
||||
.vs__dropdown-menu {
|
||||
top: calc(100% - 2px);
|
||||
left: -3px;
|
||||
width: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.selected{
|
||||
position:relative;
|
||||
top: 1.4em;
|
||||
padding: 8px 0 7px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { NODE, POD, NAMESPACE } from '@/config/types';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { removeObject } from '@/utils/array';
|
||||
|
|
@ -10,6 +11,7 @@ export default {
|
|||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
Select,
|
||||
},
|
||||
props: {
|
||||
// array of match expressions
|
||||
|
|
@ -232,7 +234,7 @@ export default {
|
|||
<div v-if="isView">
|
||||
{{ row.operator }}
|
||||
</div>
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-else
|
||||
v-model="row.operator"
|
||||
class="operator single"
|
||||
|
|
@ -253,7 +255,7 @@ export default {
|
|||
</div>
|
||||
<input v-else v-model="row.values" :mode="mode" :disabled="row.operator==='Exists' || row.operator==='DoesNotExist'" />
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="remove-container">
|
||||
<button
|
||||
v-if="!isView"
|
||||
type="button"
|
||||
|
|
@ -273,7 +275,7 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
<style lang='scss' scoped>
|
||||
$separator: 20;
|
||||
$remove: 75;
|
||||
$spacing: 10px;
|
||||
|
|
@ -297,6 +299,11 @@ export default {
|
|||
font-size:2em;
|
||||
}
|
||||
|
||||
.remove-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.selector-weight {
|
||||
color: var(--input-label)
|
||||
}
|
||||
|
|
@ -305,7 +312,6 @@ export default {
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-gap: $column-gutter;
|
||||
align-items: center;
|
||||
|
||||
&>label{
|
||||
margin-left: 8px;
|
||||
|
|
@ -315,7 +321,7 @@ export default {
|
|||
grid-template-columns: 1fr 1fr 1fr 100px;
|
||||
}
|
||||
|
||||
INPUT {
|
||||
INPUT:not(.vs__search) {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default {
|
|||
},
|
||||
extraColumns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
nameLabel: {
|
||||
|
|
@ -106,31 +106,31 @@ export default {
|
|||
const metadata = v.metadata;
|
||||
let namespace, name, description;
|
||||
|
||||
if (this.nameKey ) {
|
||||
if (this.nameKey) {
|
||||
name = get(v, this.nameKey);
|
||||
} else {
|
||||
name = metadata.name;
|
||||
}
|
||||
|
||||
if ( this.namespaced ) {
|
||||
if ( this.forceNamespace ) {
|
||||
if (this.namespaced) {
|
||||
if (this.forceNamespace) {
|
||||
namespace = this.forceNamespace;
|
||||
this.updateNamespace(namespace);
|
||||
} else if ( this.namespaceKey ) {
|
||||
} else if (this.namespaceKey) {
|
||||
namespace = get(v, this.namespaceKey);
|
||||
} else {
|
||||
namespace = metadata?.namespace;
|
||||
}
|
||||
|
||||
if ( !namespace ) {
|
||||
if (!namespace) {
|
||||
namespace = this.$store.getters['defaultNamespace'];
|
||||
if ( metadata ) {
|
||||
if (metadata) {
|
||||
metadata.namespace = namespace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( this.descriptionKey ) {
|
||||
if (this.descriptionKey) {
|
||||
description = get(v, this.descriptionKey);
|
||||
} else {
|
||||
description = metadata?.annotations?.[DESCRIPTION];
|
||||
|
|
@ -139,34 +139,39 @@ export default {
|
|||
return {
|
||||
namespace,
|
||||
name,
|
||||
description
|
||||
description,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
namespaceReallyDisabled() {
|
||||
return !!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT; // namespace is never editable
|
||||
return (
|
||||
!!this.forceNamespace || this.namespaceDisabled || this.mode === _EDIT
|
||||
); // namespace is never editable
|
||||
},
|
||||
|
||||
nameReallyDisabled() {
|
||||
return this.nameDisabled || ( this.mode === _EDIT && !this.nameEditable);
|
||||
return this.nameDisabled || (this.mode === _EDIT && !this.nameEditable);
|
||||
},
|
||||
|
||||
namespaces() {
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const choices = this.$store.getters[`${ inStore }/all`](this.namespaceType);
|
||||
|
||||
const out = sortBy(choices.map((obj) => {
|
||||
const out = sortBy(
|
||||
choices.map((obj) => {
|
||||
return {
|
||||
label: obj.nameDisplay,
|
||||
value: obj.id,
|
||||
};
|
||||
}), 'label');
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
|
||||
if ( this.forceNamespace ) {
|
||||
if (this.forceNamespace) {
|
||||
out.unshift({
|
||||
label: this.forceNamespace,
|
||||
value: this.forceNamespace
|
||||
value: this.forceNamespace,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +194,7 @@ export default {
|
|||
name(val) {
|
||||
val = val.toLowerCase();
|
||||
|
||||
if ( this.nameKey ) {
|
||||
if (this.nameKey) {
|
||||
set(this.value, this.nameKey, val);
|
||||
} else {
|
||||
this.$set(this.value.metadata, 'name', val);
|
||||
|
|
@ -203,7 +208,7 @@ export default {
|
|||
},
|
||||
|
||||
description(val) {
|
||||
if ( this.descriptionKey ) {
|
||||
if (this.descriptionKey) {
|
||||
set(this.value, this.descriptionKey, val);
|
||||
} else {
|
||||
this.value.setAnnotation(DESCRIPTION, val);
|
||||
|
|
@ -222,11 +227,11 @@ export default {
|
|||
|
||||
methods: {
|
||||
updateNamespace(val) {
|
||||
if ( this.forceNamespace ) {
|
||||
if (this.forceNamespace) {
|
||||
val = this.forceNamespace;
|
||||
}
|
||||
|
||||
if ( this.namespaceKey ) {
|
||||
if (this.namespaceKey) {
|
||||
set(this.value, this.namespaceKey, val);
|
||||
} else {
|
||||
this.value.metadata.namespace = val;
|
||||
|
|
@ -236,19 +241,20 @@ export default {
|
|||
changeNameAndNamespace(e) {
|
||||
this.name = (e.text || '').toLowerCase();
|
||||
this.namespace = e.selected;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="row mb-20">
|
||||
<div v-show="!nameNsHidden" :class="{col: true, [colSpan]: true}">
|
||||
<div v-show="!nameNsHidden" :class="{ col: true, [colSpan]: true }">
|
||||
<slot :namespaces="namespaces" name="namespace">
|
||||
<InputWithSelect
|
||||
v-if="namespaced"
|
||||
ref="name"
|
||||
class="namespace-select"
|
||||
:mode="mode"
|
||||
:disabled="namespaceReallyDisabled"
|
||||
:text-label="t(nameLabel)"
|
||||
|
|
@ -277,7 +283,7 @@ export default {
|
|||
/>
|
||||
</slot>
|
||||
</div>
|
||||
<div :class="{col: true, [colSpan]: true}">
|
||||
<div :class="{ col: true, [colSpan]: true }">
|
||||
<LabeledInput
|
||||
key="description"
|
||||
v-model="description"
|
||||
|
|
@ -287,10 +293,24 @@ export default {
|
|||
:min-height="30"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="slot in extraColumns" :key="slot" :class="{col: true, [colSpan]: true}">
|
||||
<div
|
||||
v-for="slot in extraColumns"
|
||||
:key="slot"
|
||||
:class="{ col: true, [colSpan]: true }"
|
||||
>
|
||||
<slot :name="slot">
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
.namespace-select ::v-deep {
|
||||
.labeled-select {
|
||||
min-width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -156,9 +156,9 @@ export default {
|
|||
v-if="!isView || kind!=='none'"
|
||||
v-model="kind"
|
||||
:mode="mode"
|
||||
:label="t('generic.type')"
|
||||
:label="t('probe.type.label')"
|
||||
:options="kindOptions"
|
||||
placeholder="Select a check type"
|
||||
:placeholder="t('probe.type.placeholder')"
|
||||
/>
|
||||
|
||||
<div v-if="kind && kind!=='none'" class="spacer-small" />
|
||||
|
|
@ -170,8 +170,8 @@ export default {
|
|||
min="1"
|
||||
max="65535"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.httpGet.port')"
|
||||
placeholder="e.g. 80"
|
||||
:label="t('probe.httpGet.port.label')"
|
||||
:placeholder="t('probe.httpGet.port.placeholder')"
|
||||
/>
|
||||
|
||||
<div class="spacer-small" />
|
||||
|
|
@ -179,8 +179,8 @@ export default {
|
|||
<LabeledInput
|
||||
v-model="httpGet.path"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.httpGet.path')"
|
||||
placeholder="e.g. /healthz"
|
||||
:label="t('probe.httpGet.path.label')"
|
||||
:placeholder="t('probe.httpGet.path.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -191,8 +191,8 @@ export default {
|
|||
min="1"
|
||||
max="65535"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.httpGet.port')"
|
||||
placeholder="e.g. 25"
|
||||
:label="t('probe.httpGet.port.label')"
|
||||
:placeholder="t('probe.httpGet.port.placeholderDuex')"
|
||||
/>
|
||||
<div class="spacer-small" />
|
||||
</div>
|
||||
|
|
@ -201,8 +201,8 @@ export default {
|
|||
<div class="col span-12">
|
||||
<ShellInput
|
||||
v-model="exec.command"
|
||||
:label="t('workload.container.healthCheck.command.command')"
|
||||
placeholder="e.g. cat /tmp/health"
|
||||
:label="t('probe.httpGet.port.command.label')"
|
||||
:placeholder="t('probe.httpGet.port.command.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="spacer-small" />
|
||||
|
|
@ -219,30 +219,30 @@ export default {
|
|||
<UnitInput
|
||||
v-model="probe.periodSeconds"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.checkInterval')"
|
||||
:label="t('probe.checkInterval.label')"
|
||||
min="1"
|
||||
:suffix="t('suffix.sec')"
|
||||
placeholder="Default: 10"
|
||||
:placeholder="t('probe.checkInterval.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<UnitInput
|
||||
v-model="probe.initialDelaySeconds"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.initialDelay')"
|
||||
:suffix="t('suffix.sec')"
|
||||
:label="t('probe.initialDelay.label')"
|
||||
min="0"
|
||||
placeholder="Default: 0"
|
||||
:placeholder="t('probe.initialDelay.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<UnitInput
|
||||
v-model="probe.timeoutSeconds"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.timeout')"
|
||||
:suffix="t('suffix.sec')"
|
||||
:label="t('probe.timeout.placeholder')"
|
||||
min="0"
|
||||
placeholder="Default: 3"
|
||||
:placeholder="t('probe.timeout.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -256,8 +256,8 @@ export default {
|
|||
type="number"
|
||||
min="1"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.successThreshold')"
|
||||
placeholder="Default: 1"
|
||||
:label="t('probe.successThreshold.label')"
|
||||
:placeholder="t('probe.successThreshold.placeholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
|
|
@ -266,8 +266,8 @@ export default {
|
|||
type="number"
|
||||
min="1"
|
||||
:mode="mode"
|
||||
:label="t('workload.container.healthCheck.failureThreshold')"
|
||||
placeholder="Default: 3"
|
||||
:label="t('probe.failureThreshold.label')"
|
||||
:placeholder="t('probe.failureThreshold.label')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -284,14 +284,14 @@ export default {
|
|||
:pad-left="false"
|
||||
:as-map="false"
|
||||
:read-allowed="false"
|
||||
:title="t('workload.container.healthCheck.httpGet.headers')"
|
||||
:title="t('probe.httpGet.headers.label')"
|
||||
:key-label="t('generic.name')"
|
||||
:value-label="t('generic.value')"
|
||||
:add-label="t('generic.add')"
|
||||
>
|
||||
<template #title>
|
||||
<h4>
|
||||
{{ t('workload.container.healthCheck.httpGet.headers') }}
|
||||
{{ t('probe.httpGet.headers.label') }}
|
||||
</h4>
|
||||
</template>
|
||||
</KeyValue>
|
||||
|
|
@ -308,5 +308,8 @@ export default {
|
|||
.title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
::v-deep .labeled-select {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { _VIEW } from '@/config/query-params';
|
||||
import ArrayList from '@/components/form/ArrayList';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
const OPERATOR_VALUES = {
|
||||
IS_SET: 'Exists',
|
||||
|
|
@ -15,7 +15,7 @@ export default {
|
|||
components: {
|
||||
ArrayList,
|
||||
LabeledInput,
|
||||
LabeledSelect
|
||||
Select
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -132,7 +132,7 @@ export default {
|
|||
<LabeledInput v-model="scope.row.value.key" :mode="mode" />
|
||||
</div>
|
||||
<div class="operator">
|
||||
<LabeledSelect
|
||||
<Select
|
||||
:mode="mode"
|
||||
:value="scope.row.value.operator"
|
||||
:options="operatorOptions"
|
||||
|
|
@ -147,20 +147,23 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.rule-selector {
|
||||
.box {
|
||||
& > *:not(:last-child) {
|
||||
padding-right: 10px;
|
||||
&:not(.view) table {
|
||||
table-layout: initial;
|
||||
}
|
||||
|
||||
.key, .value {
|
||||
width: 35%;
|
||||
flex: none;
|
||||
}
|
||||
::v-deep .box {
|
||||
display: grid;
|
||||
grid-template-columns: 25% 25% 25% 15%;
|
||||
column-gap: 1.75%;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.key,
|
||||
.value,
|
||||
.operator {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,88 @@
|
|||
<script>
|
||||
import { createPopper } from '@popperjs/core';
|
||||
import { get } from '@/utils/object';
|
||||
import LabeledFormElement from '@/mixins/labeled-form-element';
|
||||
import VueSelectOverrides from '@/mixins/vue-select-overrides';
|
||||
|
||||
export default {
|
||||
mixins: [LabeledFormElement, VueSelectOverrides],
|
||||
props: {
|
||||
placement: {
|
||||
disabled: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
mode: {
|
||||
default: 'edit',
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
optionKey: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
optionLabel: {
|
||||
default: 'label',
|
||||
type: String,
|
||||
},
|
||||
options: {
|
||||
default: null,
|
||||
type: Array,
|
||||
},
|
||||
placement: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
popperOverride: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
reduce: {
|
||||
default: (e) => {
|
||||
if (e && typeof e === 'object' && e.value !== undefined) {
|
||||
return e.value;
|
||||
}
|
||||
|
||||
return e;
|
||||
},
|
||||
type: Function,
|
||||
},
|
||||
searchable: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
type: [String, Object, Number, Array, Boolean],
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getOptionLabel(option) {
|
||||
if (this.$attrs['get-option-label']) {
|
||||
return this.$attrs['get-option-label'](option);
|
||||
}
|
||||
if (get(option, this.optionLabel)) {
|
||||
if (this.localizedLabel) {
|
||||
return this.$store.getters['i18n/t'](get(option, this.optionLabel));
|
||||
} else {
|
||||
return get(option, this.optionLabel);
|
||||
}
|
||||
} else {
|
||||
return option;
|
||||
}
|
||||
},
|
||||
withPopper(dropdownList, component, { width }) {
|
||||
if (this.popperOverride) {
|
||||
return this.popperOverride(dropdownList, component, { width });
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to explicitly define the dropdown width since
|
||||
* it is usually inherited from the parent with CSS.
|
||||
|
|
@ -28,12 +100,11 @@ export default {
|
|||
* above.
|
||||
*/
|
||||
const popper = createPopper(component.$refs.toggle, dropdownList, {
|
||||
|
||||
placement: this.placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: { offset: [0, -1] }
|
||||
options: { offset: [0, 2] },
|
||||
},
|
||||
{
|
||||
name: 'toggleClass',
|
||||
|
|
@ -43,7 +114,7 @@ export default {
|
|||
component.$el.setAttribute('x-placement', state.placement);
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -52,17 +123,56 @@ export default {
|
|||
*/
|
||||
return () => popper.destroy();
|
||||
},
|
||||
|
||||
focusSearch() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs['select-input'].searchEl.focus();
|
||||
});
|
||||
},
|
||||
get,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-select
|
||||
v-bind="$attrs"
|
||||
append-to-body
|
||||
:calculate-position="placement ? withPopper : undefined"
|
||||
v-on="$listeners"
|
||||
<div
|
||||
class="unlabeled-select"
|
||||
:class="{
|
||||
disabled: disabled && !isView,
|
||||
focused,
|
||||
[mode]: true,
|
||||
[status]: status,
|
||||
taggable: $attrs.taggable,
|
||||
}"
|
||||
@click="focusSearch"
|
||||
@focus="focusSearch"
|
||||
>
|
||||
<slot />
|
||||
<v-select
|
||||
v-if="!isView"
|
||||
ref="select-input"
|
||||
v-bind="$attrs"
|
||||
class="inline"
|
||||
:autoscroll="true"
|
||||
:append-to-body="!!placement"
|
||||
:calculate-position="placement ? withPopper : undefined"
|
||||
:disabled="isView || disabled"
|
||||
:get-option-key="(opt) => (optionKey ? get(opt, optionKey) : getOptionLabel(opt))"
|
||||
:get-option-label="(opt) => getOptionLabel(opt)"
|
||||
:label="optionLabel"
|
||||
:options="options"
|
||||
:map-keydown="mappedKeys"
|
||||
:placeholder="placeholder"
|
||||
:reduce="(x) => reduce(x)"
|
||||
:searchable="isSearchable"
|
||||
:value="value != null ? value : ''"
|
||||
@input="(e) => $emit('input', e)"
|
||||
@search:blur="onBlur"
|
||||
@search:focus="onFocus"
|
||||
>
|
||||
<!-- Pass down templates provided by the caller -->
|
||||
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
|
||||
<slot :name="slot" v-bind="scope" />
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import debounce from 'lodash/debounce';
|
|||
import { _EDIT, _VIEW } from '@/config/query-params';
|
||||
import { removeAt } from '@/utils/array';
|
||||
import { clone } from '@/utils/object';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
export default {
|
||||
components: { LabeledSelect },
|
||||
components: { Select },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
|
|
@ -173,7 +173,7 @@ export default {
|
|||
</div>
|
||||
<div v-if="showProtocol" class="port-protocol">
|
||||
<span v-if="isView">{{ row.protocol }}</span>
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-else
|
||||
v-model="row.protocol"
|
||||
:options="protocolOptions"
|
||||
|
|
@ -261,10 +261,22 @@ export default {
|
|||
padding: 0px;
|
||||
}
|
||||
|
||||
.ports-row INPUT {
|
||||
.ports-row {
|
||||
INPUT {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.port-protocol ::v-deep {
|
||||
.unlabeled-select {
|
||||
height: 50px;
|
||||
|
||||
.v-select.inline {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import ArrayList from '@/components/form/ArrayList';
|
||||
import { _VIEW } from '@/config/query-params';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
const EFFECT_VALUES = {
|
||||
NO_SCHEDULE: 'NoSchedule',
|
||||
|
|
@ -10,7 +10,7 @@ const EFFECT_VALUES = {
|
|||
};
|
||||
|
||||
export default {
|
||||
components: { ArrayList, LabeledSelect },
|
||||
components: { ArrayList, Select },
|
||||
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -89,7 +89,7 @@ export default {
|
|||
/>
|
||||
</td>
|
||||
<td class="effect">
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-model="scope.row.value.effect"
|
||||
:options="effectOptions"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -204,11 +204,11 @@ export default {
|
|||
:disabled="isView"
|
||||
:options="routerOptions"
|
||||
:mode="mode"
|
||||
placeholder="Select a Router..."
|
||||
:placeholder="t('target.router.placeholder')"
|
||||
:clearable="false"
|
||||
class="inline"
|
||||
:reduce="opt=>opt.value"
|
||||
label="Router"
|
||||
:label="t('target.router.label')"
|
||||
@input="update"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -220,11 +220,11 @@ export default {
|
|||
:disabled="isView"
|
||||
:mode="mode"
|
||||
:options="appOptions"
|
||||
placeholder="Select a service"
|
||||
:placeholder="t('target.service.placeholder')"
|
||||
:reduce="opt=>opt.value"
|
||||
:clearable="false"
|
||||
class="inline"
|
||||
label="Service"
|
||||
:label="t('target.service.label')"
|
||||
@input="update"
|
||||
/>
|
||||
<Checkbox v-if="kind==='app'" v-model="pickVersion" label="Target one version" class="mt-10" />
|
||||
|
|
@ -235,10 +235,10 @@ export default {
|
|||
:disabled="isView"
|
||||
:mode="mode"
|
||||
:options="versionOptions"
|
||||
placeholder="Select a version"
|
||||
:placeholder="t('target.version.placeholder')"
|
||||
:clearable="false"
|
||||
class="inline"
|
||||
label="Version"
|
||||
:label="t('target.version.label')"
|
||||
:reduce="opt=>opt.value"
|
||||
@input="update"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
import UnitInput from '@/components/form/UnitInput';
|
||||
import { _VIEW } from '@/config/query-params';
|
||||
import { random32 } from '@/utils/string';
|
||||
|
|
@ -9,7 +9,7 @@ import { random32 } from '@/utils/string';
|
|||
export default {
|
||||
components: {
|
||||
LabeledInput,
|
||||
LabeledSelect,
|
||||
Select,
|
||||
// SortableTable,
|
||||
UnitInput
|
||||
},
|
||||
|
|
@ -147,7 +147,7 @@ export default {
|
|||
<LabeledInput v-model="rule.key" :mode="mode" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<LabeledSelect
|
||||
<Select
|
||||
id="operator"
|
||||
v-model="rule.operator"
|
||||
:options="operatorOpts"
|
||||
|
|
@ -166,7 +166,7 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
<div class="col">
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-model="rule.effect"
|
||||
:options="effectOpts"
|
||||
:mode="mode"
|
||||
|
|
@ -195,24 +195,26 @@ export default {
|
|||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.tolerations{
|
||||
.tolerations{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rule, .toleration-headers{
|
||||
.rule, .toleration-headers{
|
||||
display: grid;
|
||||
grid-template-columns: 20% 10% 20% 10% 20% 10%;
|
||||
grid-gap: $column-gutter;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.rule {
|
||||
.rule {
|
||||
margin-bottom: 20px;
|
||||
.col {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.toleration-headers SPAN {
|
||||
.toleration-headers SPAN {
|
||||
color: var(--input-label);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -258,6 +258,8 @@ export default {
|
|||
:mode="mode"
|
||||
:multiple="false"
|
||||
:options="typeOpts"
|
||||
option-label="label"
|
||||
:searchable="false"
|
||||
:reduce="e=>e.value"
|
||||
:label="t('workload.container.command.fromResource.type')"
|
||||
@input="updateRow"
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export default {
|
|||
// show host port column if existing port data has any host ports defined
|
||||
const showHostPorts = !!rows.filter(row => !!row.hostPort).length;
|
||||
|
||||
return { rows, showHostPorts };
|
||||
return {
|
||||
rows,
|
||||
showHostPorts,
|
||||
workloadPortOptions: ['TCP', 'UDP']
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -188,9 +192,8 @@ export default {
|
|||
<LabeledSelect
|
||||
v-else
|
||||
v-model="row.protocol"
|
||||
:style="{'height':'50px'}"
|
||||
class="inline"
|
||||
:options="['TCP', 'UDP']"
|
||||
:options="workloadPortOptions"
|
||||
:multiple="false"
|
||||
:label="t('workload.container.ports.protocol')"
|
||||
@input="queueUpdate"
|
||||
|
|
@ -248,18 +251,18 @@ export default {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$remove: 75;
|
||||
$checkbox: 75;
|
||||
$remove: 75;
|
||||
$checkbox: 75;
|
||||
|
||||
.title {
|
||||
.title {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.read-from-file {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 1 unit is 8%
|
||||
.ports-headers, .ports-row{
|
||||
.ports-headers, .ports-row{
|
||||
display: grid;
|
||||
grid-template-columns: 30% 30% 18% 10% 5%;
|
||||
grid-column-gap: $column-gutter;
|
||||
|
|
@ -271,35 +274,46 @@ export default {
|
|||
}
|
||||
|
||||
&.show-host{
|
||||
grid-template-columns: 30% 16% 8% 16% 16% 5%;
|
||||
grid-template-columns: 30% 16% 10% 15% 15% 5%;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.add-host {
|
||||
.add-host {
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ports-headers {
|
||||
.protocol {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ports-headers {
|
||||
color: var(--input-label);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-host-ports {
|
||||
.toggle-host-ports {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.remove BUTTON {
|
||||
.remove BUTTON {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ports-row INPUT {
|
||||
.ports-row INPUT {
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
.footer {
|
||||
.protip {
|
||||
float: right;
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ports-row .protocol ::v-deep .unlabeled-select,
|
||||
.ports-row .protocol ::v-deep .unlabeled-select .v-select {
|
||||
height: 100%;
|
||||
}
|
||||
.ports-row .protocol ::v-deep .unlabeled-select .vs__dropdown-toggle {
|
||||
padding-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -12,18 +12,27 @@ export default {
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
addSuffix: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
addPrefix: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
suffix: {
|
||||
type: String,
|
||||
default: 'ago',
|
||||
},
|
||||
|
||||
tooltipPlacement: {
|
||||
type: String,
|
||||
default: 'auto'
|
||||
},
|
||||
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
|
|
@ -84,7 +93,7 @@ export default {
|
|||
const value = day(this.value);
|
||||
const now = day();
|
||||
let diff = value.diff(now, 'seconds');
|
||||
const prefix = (diff < 0 ? '' : '-');
|
||||
const prefix = (diff < 0 || !this.addPrefix ? '' : '-');
|
||||
const suffix = '';
|
||||
|
||||
diff = Math.abs(diff);
|
||||
|
|
|
|||
|
|
@ -3,17 +3,23 @@ import { MANAGEMENT } from '@/config/types';
|
|||
import { sortBy } from '@/utils/sort';
|
||||
import { findBy } from '@/utils/array';
|
||||
import { mapState } from 'vuex';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
export default {
|
||||
components: { Select },
|
||||
computed: {
|
||||
...mapState(['isMultiCluster']),
|
||||
|
||||
value: {
|
||||
get() {
|
||||
const options = this.options;
|
||||
const existing = findBy(options, 'id', this.$store.getters['clusterId']);
|
||||
const existing = findBy(
|
||||
options,
|
||||
'id',
|
||||
this.$store.getters['clusterId']
|
||||
);
|
||||
|
||||
if ( existing ) {
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +28,7 @@ export default {
|
|||
|
||||
set(neu) {
|
||||
this.$router.push({ name: 'c-cluster', params: { cluster: neu.id } });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
options() {
|
||||
|
|
@ -43,22 +49,20 @@ export default {
|
|||
methods: {
|
||||
focus() {
|
||||
this.$refs.select.$refs.search.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<v-select
|
||||
<Select
|
||||
ref="select"
|
||||
key="cluster"
|
||||
v-model="value"
|
||||
:selectable="option => option.ready"
|
||||
:selectable="(option) => option.ready"
|
||||
:clearable="false"
|
||||
:options="options"
|
||||
label="label"
|
||||
>
|
||||
<template #selected-option="opt">
|
||||
<i class="icon icon-copy icon-lg pr-5" />
|
||||
|
|
@ -74,25 +78,27 @@ export default {
|
|||
|
||||
<template #option="opt">
|
||||
<b v-if="opt === value">{{ opt.label }}</b>
|
||||
<nuxt-link v-else-if="opt.ready" class="cluster" :to="{name: 'c-cluster', params: { cluster: opt.id }}">
|
||||
<nuxt-link
|
||||
v-else-if="opt.ready"
|
||||
class="cluster"
|
||||
:to="{ name: 'c-cluster', params: { cluster: opt.id } }"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</nuxt-link>
|
||||
<span v-else class="text-muted">{{ opt.label }}</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</Select>
|
||||
<button v-shortkey.once="['c']" class="hide" @shortkey="focus()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter ::v-deep .v-select {
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
background: rgba(0,0,0,.05);
|
||||
.filter ::v-deep .unlabeled-select .v-select {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--header-btn-text);
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
|
||||
&.vs--disabled .vs__actions {
|
||||
display: none;
|
||||
|
|
@ -104,9 +110,13 @@ export default {
|
|||
}
|
||||
|
||||
.vs__dropdown-toggle {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
height: calc(var(--header-height) - 19px);
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
max-width: 100%;
|
||||
padding-top: 0;
|
||||
|
||||
.vs__actions {
|
||||
margin-left: -10px;
|
||||
|
|
@ -114,20 +124,35 @@ export default {
|
|||
}
|
||||
|
||||
.vs__selected {
|
||||
border: none;
|
||||
position: absolute;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
color: white;
|
||||
line-height: calc(var(--header-height) - 32px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT {
|
||||
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
|
||||
background: var(--dropdown-bg);
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select:not(.focused) {
|
||||
border: var(--outline-width) solid transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
|
||||
width: auto;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.filter ::v-deep .v-select.inline.vs--single.vs--open .vs__search {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT:hover {
|
||||
.filter ::v-deep .unlabeled-select INPUT:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -172,19 +172,20 @@ export default {
|
|||
}
|
||||
|
||||
> .apps {
|
||||
padding: 0 0 0 5px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
> .cluster {
|
||||
grid-area: cluster;
|
||||
background-color: var(--header-bg);
|
||||
position: relative;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
> .top {
|
||||
grid-area: top;
|
||||
background-color: var(--header-bg);
|
||||
padding-top: 8px;
|
||||
padding-top: 6px;
|
||||
|
||||
INPUT[type='search']::placeholder,
|
||||
.vs__open-indicator,
|
||||
|
|
|
|||
|
|
@ -3,60 +3,35 @@ import { NAMESPACE_FILTERS } from '@/store/prefs';
|
|||
import { NAMESPACE, MANAGEMENT } from '@/config/types';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
import { isArray, addObjects, findBy, filterBy } from '@/utils/array';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
value: {
|
||||
get() {
|
||||
const values = this.$store.getters['prefs/get'](NAMESPACE_FILTERS);
|
||||
const options = this.options;
|
||||
components: { Select },
|
||||
|
||||
if ( !values ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove values that are not valid options
|
||||
const out = values.map((value) => {
|
||||
return findBy(options, 'id', value);
|
||||
}).filter(x => !!x);
|
||||
|
||||
return out;
|
||||
data() {
|
||||
return {
|
||||
isHovered: false,
|
||||
hoveredTimeout: null,
|
||||
maskedWidth: null,
|
||||
};
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
const old = (this.value || []).slice();
|
||||
computed: {
|
||||
filterIsHovered() {
|
||||
return this.isHovered;
|
||||
},
|
||||
|
||||
neu = neu.filter(x => !!x.id);
|
||||
maskedDropdownWidth() {
|
||||
const refs = this.$refs;
|
||||
const select = refs.select;
|
||||
|
||||
const last = neu[neu.length - 1];
|
||||
const lastIsSpecial = last?.kind === 'special';
|
||||
const hadUser = old.find(x => x.id === 'all://user');
|
||||
const hadAll = old.find(x => x.id === 'all');
|
||||
if (select) {
|
||||
const selectWidth = select.$el.offsetWidth;
|
||||
|
||||
if ( lastIsSpecial ) {
|
||||
neu = [last];
|
||||
return selectWidth;
|
||||
}
|
||||
|
||||
if ( neu.length > 1 ) {
|
||||
neu = neu.filter(x => x.kind !== 'special');
|
||||
}
|
||||
|
||||
if ( neu.find(x => x.id === 'all') ) {
|
||||
neu = [];
|
||||
}
|
||||
|
||||
let ids;
|
||||
|
||||
// If there as something selected and you remove it, go back to user by default
|
||||
// Unless it was user or all
|
||||
if (neu.length === 0 && !hadUser && !hadAll ) {
|
||||
ids = ['all://user'];
|
||||
} else {
|
||||
ids = neu.map(x => x.id);
|
||||
}
|
||||
|
||||
this.$store.dispatch('switchNamespaces', ids);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
options() {
|
||||
|
|
@ -87,40 +62,47 @@ export default {
|
|||
id: 'namespaced://false',
|
||||
kind: 'special',
|
||||
label: t('nav.ns.clusterLevel'),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
divider();
|
||||
|
||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||
const namespaces = sortBy(this.$store.getters[`${ inStore }/all`](NAMESPACE), ['nameDisplay']);
|
||||
const namespaces = sortBy(
|
||||
this.$store.getters[`${ inStore }/all`](NAMESPACE),
|
||||
['nameDisplay']
|
||||
);
|
||||
|
||||
if ( this.$store.getters['isMultiCluster'] ) {
|
||||
if (this.$store.getters['isMultiCluster']) {
|
||||
const cluster = this.$store.getters['currentCluster'];
|
||||
let projects = this.$store.getters['management/all'](MANAGEMENT.PROJECT);
|
||||
let projects = this.$store.getters['management/all'](
|
||||
MANAGEMENT.PROJECT
|
||||
);
|
||||
|
||||
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), ['nameDisplay']);
|
||||
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
|
||||
'nameDisplay',
|
||||
]);
|
||||
const projectsById = {};
|
||||
const namespacesByProject = {};
|
||||
let firstProject = true;
|
||||
|
||||
namespacesByProject[null] = []; // For namespaces not in a project
|
||||
|
||||
for ( const project of projects ) {
|
||||
for (const project of projects) {
|
||||
projectsById[project.metadata.name] = project;
|
||||
}
|
||||
|
||||
for (const namespace of namespaces ) {
|
||||
for (const namespace of namespaces) {
|
||||
let projectId = namespace.projectId;
|
||||
|
||||
if ( !projectId || !projectsById[projectId] ) {
|
||||
if (!projectId || !projectsById[projectId]) {
|
||||
// If there's a projectId but that project doesn't exist, treat it like no project
|
||||
projectId = null;
|
||||
}
|
||||
|
||||
let entry = namespacesByProject[namespace.projectId];
|
||||
|
||||
if ( !entry ) {
|
||||
if (!entry) {
|
||||
entry = [];
|
||||
namespacesByProject[namespace.projectId] = entry;
|
||||
}
|
||||
|
|
@ -128,10 +110,10 @@ export default {
|
|||
entry.push(namespace);
|
||||
}
|
||||
|
||||
for ( const project of projects ) {
|
||||
for (const project of projects) {
|
||||
const id = project.metadata.name;
|
||||
|
||||
if ( firstProject ) {
|
||||
if (firstProject) {
|
||||
firstProject = false;
|
||||
} else {
|
||||
divider();
|
||||
|
|
@ -150,8 +132,8 @@ export default {
|
|||
|
||||
const orphans = namespacesByProject[null];
|
||||
|
||||
if ( orphans.length ) {
|
||||
if ( !firstProject ) {
|
||||
if (orphans.length) {
|
||||
if (!firstProject) {
|
||||
divider();
|
||||
}
|
||||
|
||||
|
|
@ -171,17 +153,20 @@ export default {
|
|||
return out;
|
||||
|
||||
function addNamespace(namespaces) {
|
||||
if ( !isArray(namespaces) ) {
|
||||
if (!isArray(namespaces)) {
|
||||
namespaces = [namespaces];
|
||||
}
|
||||
|
||||
addObjects(out, namespaces.map((namespace) => {
|
||||
addObjects(
|
||||
out,
|
||||
namespaces.map((namespace) => {
|
||||
return {
|
||||
id: `ns://${ namespace.id }`,
|
||||
kind: 'namespace',
|
||||
label: t('nav.ns.namespace', { name: namespace.nameDisplay }),
|
||||
};
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function divider() {
|
||||
|
|
@ -191,81 +176,147 @@ export default {
|
|||
disabled: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
value: {
|
||||
get() {
|
||||
const values = this.$store.getters['prefs/get'](NAMESPACE_FILTERS);
|
||||
const options = this.options;
|
||||
|
||||
if (!values) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Remove values that are not valid options
|
||||
const out = values
|
||||
.map((value) => {
|
||||
return findBy(options, 'id', value);
|
||||
})
|
||||
.filter(x => !!x);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
set(neu) {
|
||||
const old = (this.value || []).slice();
|
||||
|
||||
neu = neu.filter(x => !!x.id);
|
||||
|
||||
const last = neu[neu.length - 1];
|
||||
const lastIsSpecial = last?.kind === 'special';
|
||||
const hadUser = old.find(x => x.id === 'all://user');
|
||||
const hadAll = old.find(x => x.id === 'all');
|
||||
|
||||
if (lastIsSpecial) {
|
||||
neu = [last];
|
||||
}
|
||||
|
||||
if (neu.length > 1) {
|
||||
neu = neu.filter(x => x.kind !== 'special');
|
||||
}
|
||||
|
||||
if (neu.find(x => x.id === 'all')) {
|
||||
neu = [];
|
||||
}
|
||||
|
||||
let ids;
|
||||
|
||||
// If there as something selected and you remove it, go back to user by default
|
||||
// Unless it was user or all
|
||||
if (neu.length === 0 && !hadUser && !hadAll) {
|
||||
ids = ['all://user'];
|
||||
} else {
|
||||
ids = neu.map(x => x.id);
|
||||
}
|
||||
|
||||
this.$store.dispatch('switchNamespaces', ids);
|
||||
},
|
||||
},
|
||||
|
||||
showGroupedOptions() {
|
||||
if (this.value && this.value.length >= 2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.maskedWidth = this.maskedDropdownWidth;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
this.$refs.select.$refs.search.focus();
|
||||
this.$refs.select.$refs['select-input'].searchEl.focus();
|
||||
},
|
||||
focusHandler(event) {
|
||||
if (event === 'selectBlurred') {
|
||||
this.maskedWidth = this.maskedDropdownWidth;
|
||||
this.isHovered = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// we dont handle blur here because the select specifically handles its blur events and emits when it does.
|
||||
// we should listen to this blur event because the swapping of the masked select with real select.
|
||||
if (event.type === 'focus') {
|
||||
this.isHovered = true;
|
||||
this.$nextTick(() => {
|
||||
this.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isHoveredHandler(event) {
|
||||
clearTimeout(this.hoveredTimeout);
|
||||
|
||||
if (event.type === 'mouseenter') {
|
||||
this.isHovered = true;
|
||||
} else if (event.type === 'mouseleave') {
|
||||
this.hoveredTimeout = setTimeout(() => {
|
||||
this.maskedWidth = this.maskedDropdownWidth;
|
||||
this.isHovered = false;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.filter ::v-deep .v-select {
|
||||
min-width: 220px;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter ::v-deep .v-select .vs__selected {
|
||||
margin: 4px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: solid white thin;
|
||||
color: white;
|
||||
height: calc(var(--header-height) - 26px);
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT {
|
||||
width: auto;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__search::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__dropdown-toggle {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
height: calc(var(--header-height) - 16px);
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__deselect:after {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .v-select .vs__actions:after {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT[type='search'] {
|
||||
padding: 7px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<v-select
|
||||
<div
|
||||
class="filter"
|
||||
:class="{'show-masked': showGroupedOptions && !filterIsHovered}"
|
||||
@mouseenter="isHoveredHandler"
|
||||
@mouseleave="isHoveredHandler"
|
||||
>
|
||||
<div tabindex="0" class="unlabeled-select masked-dropdown" @focus="focusHandler">
|
||||
<div class="v-select inline vs--searchable">
|
||||
<div class="vs__dropdown-toggle">
|
||||
<div class="vs__selected-options">
|
||||
<div class="vs__selected">
|
||||
{{ t('namespaceFilter.selected.label', { total: value.length }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vs__actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Select
|
||||
ref="select"
|
||||
v-model="value"
|
||||
:class="{
|
||||
'has-more': showGroupedOptions,
|
||||
}"
|
||||
multiple
|
||||
:placeholder="t('nav.ns.all')"
|
||||
:selectable="option => !option.disabled && option.id"
|
||||
:selectable="(option) => !option.disabled && option.id"
|
||||
:options="options"
|
||||
label="label"
|
||||
:close-on-select="false"
|
||||
@on-blur="focusHandler('selectBlurred')"
|
||||
>
|
||||
<template v-slot:option="opt">
|
||||
<template v-if="opt.kind === 'namespace'">
|
||||
|
|
@ -281,7 +332,99 @@ export default {
|
|||
{{ opt.label }}
|
||||
</template>
|
||||
</template>
|
||||
</v-select>
|
||||
</Select>
|
||||
<button v-shortkey.once="['n']" class="hide" @shortkey="focus()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.filter {
|
||||
min-width: 220px;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter.show-masked ::v-deep .unlabeled-select:not(.masked-dropdown) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.filter:not(.show-masked) ::v-deep .unlabeled-select.masked-dropdown {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select.has-more .v-select:not(.vs--open) .vs__dropdown-toggle {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select.has-more .v-select.vs--open .vs__dropdown-toggle {
|
||||
height: max-content;
|
||||
background-color: var(--header-bg);
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select:not(.focused) {
|
||||
border: var(--outline-width) solid transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
|
||||
background: var(--dropdown-bg);
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .v-select.inline {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .v-select .vs__selected {
|
||||
margin: 4px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: solid white thin;
|
||||
color: white;
|
||||
height: calc(var(--header-height) - 26px);
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__search::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__dropdown-toggle {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
height: calc(var(--header-height) - 16px);
|
||||
max-width: 100%;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__deselect:after {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .v-select .vs__actions:after {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
|
||||
padding: 7px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ import { ucFirst } from '@/utils/string';
|
|||
import { createPopper } from '@popperjs/core';
|
||||
import $ from 'jquery';
|
||||
import { CATALOG } from '@/config/types';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
export default {
|
||||
components: { Select },
|
||||
|
||||
data() {
|
||||
return { previous: null };
|
||||
},
|
||||
|
|
@ -162,6 +165,10 @@ export default {
|
|||
const popper = createPopper(component.$refs.toggle, dropdownList, {
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: { offset: [0, -2] },
|
||||
},
|
||||
{
|
||||
name: 'toggleClass',
|
||||
enabled: true,
|
||||
|
|
@ -181,18 +188,17 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<v-select
|
||||
<Select
|
||||
ref="select"
|
||||
key="product"
|
||||
:value="value"
|
||||
:clearable="false"
|
||||
:searchable="false"
|
||||
:selectable="option => !option.disabled"
|
||||
:options="options"
|
||||
:reduce="opt=>opt.value"
|
||||
:calculate-position="withPopper"
|
||||
:popper-override="withPopper"
|
||||
:append-to-body="true"
|
||||
|
||||
label="label"
|
||||
placement="bottom"
|
||||
@input="change"
|
||||
>
|
||||
<template v-slot:option="opt">
|
||||
|
|
@ -205,7 +211,7 @@ export default {
|
|||
<i v-if="opt.kind === 'external'" class="icon icon-external-link ml-10" />
|
||||
</template>
|
||||
</template>
|
||||
</v-select>
|
||||
</Select>
|
||||
<button v-shortkey.once="['p']" class="hide" @shortkey="focus()" />
|
||||
<button v-shortkey.once="['f']" class="hide" @shortkey="switchToFleet()" />
|
||||
<button v-shortkey.once="['e']" class="hide" @shortkey="switchToExplorer()" />
|
||||
|
|
@ -214,53 +220,58 @@ export default {
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.filter ::v-deep .v-select {
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
.filter ::v-deep .unlabeled-select {
|
||||
background-color: transparent;
|
||||
&:not(.focused) {
|
||||
border: var(--outline-width) solid transparent;
|
||||
}
|
||||
.v-select {
|
||||
&.vs--disabled .vs__actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vs__actions:after {
|
||||
.vs__actions {
|
||||
&:after {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vs__dropdown-toggle {
|
||||
margin-bottom: -4px;
|
||||
height: var(--header-height);
|
||||
// margin-left: 35px;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
// left: 35px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.vs__selected {
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
color: white;
|
||||
line-height: calc(var(--header-height) - 14px);
|
||||
line-height: calc(var(--header-height) - 7px);
|
||||
position: relative;
|
||||
left: 40px;
|
||||
align-self: center;
|
||||
}
|
||||
.vs__selected-options {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.unlabeled-select INPUT[type='search'] {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT {
|
||||
width: auto;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep INPUT:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.product-menu {
|
||||
// these styles exist because the dd is placed with Popper and is outside the flow of the component, product-menu gets appended to the menu
|
||||
.product-menu {
|
||||
width: 300px;
|
||||
max-height: 90vh;
|
||||
&.vs__dropdown-menu {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vs__dropdown-option {
|
||||
padding: 10px;
|
||||
|
|
@ -284,7 +295,6 @@ export default {
|
|||
|
||||
.vs__dropdown-option--selected {
|
||||
color: var(--body-text);
|
||||
// font-weight: bold;
|
||||
background: var(--nav-active);
|
||||
border-left: 5px solid var(--primary);
|
||||
|
||||
|
|
@ -292,5 +302,5 @@ export default {
|
|||
color: var(--product-icon-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ export default {
|
|||
v-if="containerChoices.length > 0"
|
||||
v-model="container"
|
||||
:disabled="containerChoices.length === 1"
|
||||
class="containerPicker auto-width"
|
||||
class="containerPicker"
|
||||
:options="containerChoices"
|
||||
:clearable="false"
|
||||
placement="top"
|
||||
|
|
@ -560,11 +560,11 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.containerPicker ::v-deep .vs__search,
|
||||
.range ::v-deep .vs__search {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
.containerPicker {
|
||||
::v-deep &.unlabeled-select {
|
||||
display: inline-block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -268,8 +268,8 @@ export default {
|
|||
:disabled="containerChoices.length === 1"
|
||||
class="containerPicker auto-width"
|
||||
:options="containerChoices"
|
||||
:searchable="false"
|
||||
:clearable="false"
|
||||
placement="top"
|
||||
@input="switchTo($event)"
|
||||
>
|
||||
<template #selected-option="option">
|
||||
|
|
@ -309,10 +309,10 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.containerPicker ::v-deep .vs__search {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
.containerPicker {
|
||||
::v-deep &.unlabeled-select {
|
||||
display: inline-block;
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export default {
|
|||
set(value) {
|
||||
this.$store.commit('updateWorkspace', { value });
|
||||
this.$store.dispatch('prefs/set', { key: WORKSPACE, value });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
options() {
|
||||
|
|
@ -29,7 +29,7 @@ export default {
|
|||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -38,48 +38,8 @@ export default {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.filter ::v-deep .v-select {
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter ::v-deep .v-select .vs__selected {
|
||||
margin: 4px;
|
||||
user-select: none;
|
||||
color: white;
|
||||
height: calc(var(--header-height) - 26px);
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__dropdown-toggle {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
height: calc(var(--header-height) - 16px);
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__deselect:after {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .v-select .vs__actions:after {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.filter ::v-deep .vs__search {
|
||||
width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="filter">
|
||||
<Select
|
||||
|
|
@ -88,8 +48,69 @@ export default {
|
|||
label="label"
|
||||
:options="options"
|
||||
:clearable="false"
|
||||
:reduce="opt=>opt.value"
|
||||
:reduce="(opt) => opt.value"
|
||||
/>
|
||||
<button v-shortkey.once="['w']" class="hide" @shortkey="focus()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="scss" scoped>
|
||||
.filter {
|
||||
min-width: 220px;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select:not(.focused) {
|
||||
border: var(--outline-width) solid transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
|
||||
background: var(--dropdown-bg);
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .v-select.inline {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT {
|
||||
width: auto;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__search::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__dropdown-toggle {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--header-btn-bg);
|
||||
color: var(--header-btn-text);
|
||||
height: calc(var(--header-height) - 16px);
|
||||
max-width: 100%;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .vs__deselect:after {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select .v-select .vs__actions:after {
|
||||
fill: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
|
||||
padding: 7px;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -44,29 +44,44 @@ export function init(store) {
|
|||
},
|
||||
{
|
||||
name: 'total',
|
||||
label: 'Total',
|
||||
labelKey: 'cis.scan.total',
|
||||
value: 'status.summary.total',
|
||||
},
|
||||
{
|
||||
name: 'pass',
|
||||
label: 'Pass',
|
||||
value: 'status.summary.pass',
|
||||
labelKey: 'cis.scan.pass',
|
||||
},
|
||||
{
|
||||
name: 'fail',
|
||||
label: 'Fail',
|
||||
labelKey: 'cis.scan.fail',
|
||||
value: 'status.summary.fail',
|
||||
},
|
||||
{
|
||||
name: 'warn',
|
||||
labelKey: 'cis.scan.warn',
|
||||
value: 'status.summary.warn',
|
||||
},
|
||||
{
|
||||
name: 'skip',
|
||||
label: 'Skip',
|
||||
labelKey: 'cis.scan.skip',
|
||||
value: 'status.summary.skip',
|
||||
},
|
||||
{
|
||||
name: 'notApplicable',
|
||||
label: 'N/A',
|
||||
labelKey: 'cis.scan.notApplicable',
|
||||
value: 'status.summary.notApplicable',
|
||||
},
|
||||
{
|
||||
name: 'nextScanAt',
|
||||
label: 'Next Scan',
|
||||
value: 'status.NextScanAt',
|
||||
formatter: 'LiveDate',
|
||||
formatterOpts: { addPrefix: false },
|
||||
sort: 'status.nextScanAt:desc',
|
||||
width: 150,
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
name: 'lastRunTimestamp',
|
||||
label: 'Last Run',
|
||||
|
|
@ -79,4 +94,22 @@ export function init(store) {
|
|||
defaultSort: true,
|
||||
},
|
||||
]);
|
||||
|
||||
headers(CIS.CLUSTER_SCAN_PROFILE, [
|
||||
STATE,
|
||||
NAME_HEADER,
|
||||
{
|
||||
name: 'benchmarkVersion',
|
||||
labelKey: 'cis.benchmarkVersion',
|
||||
value: 'spec.benchmarkVersion',
|
||||
formatter: 'Link',
|
||||
formatterOpts: { options: { internal: true }, to: { name: 'c-cluster-product-resource-id', params: { resource: CIS.BENCHMARK } } },
|
||||
},
|
||||
{
|
||||
name: 'skippedTests',
|
||||
labelKey: 'cis.testsSkipped',
|
||||
value: 'numberTestsSkipped',
|
||||
sort: ['numberTestsSkipped']
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,20 @@
|
|||
import Date from '@/components/formatter/Date';
|
||||
import SortableTable from '@/components/SortableTable';
|
||||
import Banner from '@/components/Banner';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import day from 'dayjs';
|
||||
import { DATE_FORMAT, TIME_FORMAT } from '@/store/prefs';
|
||||
import { escapeHtml, randomStr } from '@/utils/string';
|
||||
import { defaultAsyncData } from '@/components/ResourceDetail';
|
||||
import { CIS } from '@/config/types';
|
||||
import { STATE } from '@/config/table-headers';
|
||||
import { randomStr } from '@/utils/string';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Date,
|
||||
SortableTable,
|
||||
Banner
|
||||
Banner,
|
||||
LabeledSelect
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -24,7 +28,7 @@ export default {
|
|||
},
|
||||
|
||||
async fetch() {
|
||||
this.clusterReport = await this.value.getReport();
|
||||
this.clusterReports = await this.value.getReports();
|
||||
},
|
||||
|
||||
asyncData(ctx) {
|
||||
|
|
@ -32,14 +36,12 @@ export default {
|
|||
},
|
||||
|
||||
data() {
|
||||
return { clusterReport: null };
|
||||
return { clusterReports: [], clusterReport: null };
|
||||
},
|
||||
|
||||
computed: {
|
||||
parsedReport() {
|
||||
const report = this.clusterReport?.parsedReport;
|
||||
|
||||
return report;
|
||||
return this.clusterReport?.parsedReport || null;
|
||||
},
|
||||
|
||||
reportNodes() {
|
||||
|
|
@ -96,15 +98,19 @@ export default {
|
|||
value: this.parsedReport.total
|
||||
},
|
||||
{
|
||||
label: this.t('cis.scan.passed'),
|
||||
label: this.t('cis.scan.pass'),
|
||||
value: this.parsedReport.pass
|
||||
},
|
||||
{
|
||||
label: this.t('cis.scan.skipped'),
|
||||
label: this.t('cis.scan.warn'),
|
||||
value: this.parsedReport.warn
|
||||
},
|
||||
{
|
||||
label: this.t('cis.scan.skip'),
|
||||
value: this.parsedReport.skip
|
||||
},
|
||||
{
|
||||
label: this.t('cis.scan.failed'),
|
||||
label: this.t('cis.scan.fail'),
|
||||
value: this.parsedReport.fail
|
||||
},
|
||||
{
|
||||
|
|
@ -168,14 +174,25 @@ export default {
|
|||
watch: {
|
||||
value(neu) {
|
||||
try {
|
||||
neu.getReport().then((report) => {
|
||||
this.clusterReport = report;
|
||||
neu.getReports().then((reports) => {
|
||||
this.clusterReports = reports;
|
||||
});
|
||||
} catch {}
|
||||
},
|
||||
|
||||
clusterReports(neu) {
|
||||
this.clusterReport = neu[0];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
reportLabel(report = {}) {
|
||||
const { creationTimestamp } = report.metadata;
|
||||
const dateFormat = escapeHtml( this.$store.getters['prefs/get'](DATE_FORMAT));
|
||||
const timeFormat = escapeHtml( this.$store.getters['prefs/get'](TIME_FORMAT));
|
||||
|
||||
return day(creationTimestamp).format(`${ dateFormat } ${ timeFormat }`);
|
||||
},
|
||||
|
||||
nodeState(check, node, nodes = []) {
|
||||
if (check.state === 'mixed') {
|
||||
|
|
@ -187,11 +204,13 @@ export default {
|
|||
|
||||
testStateSort(state) {
|
||||
const SORT_ORDER = {
|
||||
fail: 1,
|
||||
skip: 2,
|
||||
notApplicable: 3,
|
||||
other: 7,
|
||||
notApplicable: 6,
|
||||
skip: 5,
|
||||
pass: 4,
|
||||
other: 5,
|
||||
warn: 3,
|
||||
mixed: 2,
|
||||
fail: 1,
|
||||
};
|
||||
|
||||
return `${ SORT_ORDER[state] || SORT_ORDER['other'] } ${ state }`;
|
||||
|
|
@ -218,8 +237,17 @@ export default {
|
|||
<span v-else>{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="clusterReports.length > 1" class="table-header row mb-20">
|
||||
<div class="col span-8">
|
||||
<h3 class="mb-0">
|
||||
{{ t('cis.scan.scanReport') }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col span-4">
|
||||
<v-select v-model="clusterReport" class="inline" :options="clusterReports" :get-option-label="reportLabel" :get-option-key="report=>report.id" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="results.length">
|
||||
<h3>{{ t('cis.scan.scanReport') }}</h3>
|
||||
<SortableTable
|
||||
default-sort-by="state"
|
||||
:search="false"
|
||||
|
|
@ -269,4 +297,8 @@ export default {
|
|||
.sub-table {
|
||||
padding: 0px 40px 0px 40px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
align-items: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
<script>
|
||||
import CruResource from '@/components/CruResource';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import Banner from '@/components/Banner';
|
||||
import Loading from '@/components/Loading';
|
||||
import { CIS, CONFIG_MAP } from '@/config/types';
|
||||
import { CIS, CONFIG_MAP, ENDPOINTS } from '@/config/types';
|
||||
import { mapGetters } from 'vuex';
|
||||
import createEditView from '@/mixins/create-edit-view';
|
||||
import { allHash } from '@/utils/promise';
|
||||
import Checkbox from '@/components/form/Checkbox';
|
||||
import RadioGroup from '@/components/form/RadioGroup';
|
||||
import cronstrue from 'cronstrue';
|
||||
|
||||
const semver = require('semver');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CruResource, LabeledSelect, Banner, Loading
|
||||
CruResource, LabeledSelect, Banner, Loading, Checkbox, LabeledInput, RadioGroup
|
||||
},
|
||||
|
||||
mixins: [createEditView],
|
||||
|
|
@ -33,9 +38,17 @@ export default {
|
|||
const hash = await allHash({
|
||||
profiles: this.$store.dispatch('cluster/findAll', { type: CIS.CLUSTER_SCAN_PROFILE }),
|
||||
benchmarks: this.$store.dispatch('cluster/findAll', { type: CIS.BENCHMARK }),
|
||||
defaultConfigMap: this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: 'cis-operator-system/default-clusterscanprofiles' })
|
||||
defaultConfigMap: this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: 'cis-operator-system/default-clusterscanprofiles' }),
|
||||
});
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: 'cattle-monitoring-system/rancher-monitoring-alertmanager' });
|
||||
|
||||
this.hasAlertManager = true;
|
||||
} catch {
|
||||
this.hasAlertManager = false;
|
||||
}
|
||||
|
||||
this.allProfiles = hash.profiles;
|
||||
this.defaultConfigMap = hash.defaultConfigMap;
|
||||
},
|
||||
|
|
@ -44,18 +57,31 @@ export default {
|
|||
if (!this.value.metadata.name) {
|
||||
this.value.metadata.generateName = 'scan-';
|
||||
}
|
||||
if (!this.value.spec) {
|
||||
this.value.spec = { scanProfileName: null };
|
||||
}
|
||||
|
||||
return {
|
||||
allProfiles: [], defaultConfigMap: null, scanProfileName: this.value.spec.scanProfileName
|
||||
allProfiles: [], defaultConfigMap: null, scanAlertRule: this.value.spec.scanAlertRule, hasAlertManager: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({ currentCluster: 'currentCluster', t: 'i18n/t' }),
|
||||
|
||||
cronLabel() {
|
||||
const { cronSchedule } = this.value.spec;
|
||||
|
||||
if (!cronSchedule) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const hint = cronstrue.toString(cronSchedule);
|
||||
|
||||
return hint;
|
||||
} catch (e) {
|
||||
return 'invalid cron expression';
|
||||
}
|
||||
},
|
||||
|
||||
validProfiles() {
|
||||
const profileNames = this.allProfiles.filter((profile) => {
|
||||
const benchmarkVersion = profile?.spec?.benchmarkVersion;
|
||||
|
|
@ -82,13 +108,20 @@ export default {
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
monitoringUrl() {
|
||||
return this.$router.resolve({
|
||||
name: 'c-cluster-monitoring',
|
||||
params: { cluster: this.$route.params.cluster }
|
||||
}).href;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
defaultProfile(neu) {
|
||||
if (neu && !this.scanProfileName) {
|
||||
this.scanProfileName = neu?.id;
|
||||
if (neu && !this.value.spec.scanProfileName) {
|
||||
this.value.spec.scanProfileName = neu?.id;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -122,26 +155,56 @@ export default {
|
|||
|
||||
<CruResource
|
||||
v-else
|
||||
:validation-passed="!!scanProfileName"
|
||||
:validation-passed="!!value.spec.scanProfileName"
|
||||
:done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<template>
|
||||
<Banner v-if="!validProfiles.length" color="warning" :label="t('cis.noProfiles')" />
|
||||
|
||||
<div v-else class="row">
|
||||
<div v-else class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
v-model="scanProfileName"
|
||||
v-model="value.spec.scanProfileName"
|
||||
:mode="mode"
|
||||
:label="t('cis.profile')"
|
||||
:options="validProfiles"
|
||||
@input="value.spec.scanProfileName = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Scheduling</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="value.spec.cronSchedule" :mode="mode" :label="t('cis.cronSchedule.label')" :placeholder="t('cis.cronSchedule.placeholder')" />
|
||||
<span class="text-muted">{{ cronLabel }}</span>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model.number="value.spec.retention" type="number" :mode="mode" :label="t('cis.retention')" />
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
Alerting
|
||||
</h3>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-12">
|
||||
<Banner v-if="scanAlertRule.alertOnFailure || scanAlertRule.alertOnComplete" class="mt-0" :color="hasAlertManager ? 'info' : 'warning'">
|
||||
<span v-if="!hasAlertManager" v-html="t('cis.alertNotFound')" />
|
||||
<span v-html="t('cis.alertNeeded', {link: monitoringUrl}, true)" />
|
||||
</banner>
|
||||
<Checkbox v-model="scanAlertRule.alertOnComplete" :label="t('cis.alertOnComplete')" />
|
||||
<Checkbox v-model="scanAlertRule.alertOnFailure" :label="t('cis.alertOnFailure')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<span>{{ t('cis.scoreWarning.label') }}</span> <i v-tooltip="t('cis.scoreWarning.protip')" class="icon icon-info" />
|
||||
<RadioGroup v-model="value.spec.scoreWarning" name="scoreWarning" :options="['pass', 'fail']" :labels="[t('cis.scan.pass'), t('cis.scan.fail')]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</CruResource>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,15 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<CruResource :validation-passed="!!name && !!value.spec.benchmarkVersion" :done-route="doneRoute" :resource="value" :mode="mode" @finish="save">
|
||||
<CruResource
|
||||
:validation-passed="!!name && !!value.spec.benchmarkVersion"
|
||||
done-route="doneRoute"
|
||||
:resource="value"
|
||||
:mode="mode"
|
||||
:errors="errors"
|
||||
@finish="save"
|
||||
@error="e=>errors = e"
|
||||
>
|
||||
<template>
|
||||
<div class="spacer"></div>
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ export default {
|
|||
return {
|
||||
selectedSeverityLabel: null,
|
||||
ignoredAnnotations: INGORED_ANNOTATIONS,
|
||||
severityOptions: [
|
||||
this.t('prometheusRule.alertingRules.labels.severity.choices.critical'),
|
||||
this.t('prometheusRule.alertingRules.labels.severity.choices.warning'),
|
||||
this.t('prometheusRule.alertingRules.labels.severity.choices.none'),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -336,11 +341,7 @@ export default {
|
|||
:mode="mode"
|
||||
:label="t('prometheusRule.alertingRules.labels.severity.choices.label')"
|
||||
:localized-label="false"
|
||||
:options="[
|
||||
t('prometheusRule.alertingRules.labels.severity.choices.critical'),
|
||||
t('prometheusRule.alertingRules.labels.severity.choices.warning'),
|
||||
t('prometheusRule.alertingRules.labels.severity.choices.none'),
|
||||
]"
|
||||
:options="severityOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,13 +67,13 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<Banner v-if="value.isRoot" color="info">
|
||||
This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted.
|
||||
{{ t("monitoringRoute.info") }}
|
||||
</Banner>
|
||||
<Tabbed ref="tabbed" :side-tabs="true" default-tab="overview">
|
||||
<Tab label="Receiver" :weight="2" name="receiver">
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect v-model="value.spec.receiver" :options="receiverOptions" label="Receiver" :mode="mode" />
|
||||
<LabeledSelect v-model="value.spec.receiver" :options="receiverOptions" :label="t('monitoringRoute.receiver.label')" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
|
|
@ -81,9 +81,9 @@ export default {
|
|||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<span class="label">
|
||||
Group By:
|
||||
{{ t("monitoringRoute.groups.label") }}:
|
||||
</span>
|
||||
<ArrayList v-if="!isView || value.spec.group_by.length > 0" v-model="value.spec.group_by" label="Group By" :mode="mode" :initial-empty-row="true" />
|
||||
<ArrayList v-if="!isView || value.spec.group_by.length > 0" v-model="value.spec.group_by" :label="t('monitoringRoute.groups.label')" :mode="mode" :initial-empty-row="true" />
|
||||
<div v-else>
|
||||
{{ t('generic.none') }}
|
||||
</div>
|
||||
|
|
@ -92,32 +92,33 @@ export default {
|
|||
<hr class="divider" />
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="value.spec.group_wait" label="Group Wait" :mode="mode" />
|
||||
<LabeledInput v-model="value.spec.group_wait" :label="t('monitoringRoute.wait.label')" :mode="mode" />
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="value.spec.group_interval" label="Group Interval" :mode="mode" />
|
||||
<LabeledInput v-model="value.spec.group_interval" :label="t('monitoringRoute.interval.label')" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabeledInput v-model="value.spec.repeat_interval" label="Repeat Interval" :mode="mode" />
|
||||
<LabeledInput v-model="value.spec.repeat_interval" :label="t('monitoringRoute.repeatInterval.label')" :mode="mode" />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab label="Matching" :weight="1" name="matching">
|
||||
<Banner v-if="value.isRoot" color="info">
|
||||
The root route has to match everything so matching can't be configured.
|
||||
{{ t('monitoringRoute.matching.info') }}
|
||||
</Banner>
|
||||
<div v-else class="row">
|
||||
<div class="col span-12">
|
||||
<span class="label">
|
||||
Match:
|
||||
{{ t('monitoringRoute.matching.label') }}
|
||||
</span>
|
||||
<KeyValue
|
||||
v-if="!isView || Object.keys(value.spec.match || {}).length > 0"
|
||||
v-model="value.spec.match"
|
||||
:disabled="value.isRoot"
|
||||
:options="receiverOptions"
|
||||
:label="t('monitoringRoute.receiver.label')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
add-label="Add match"
|
||||
|
|
@ -130,13 +131,14 @@ export default {
|
|||
<div class="row mt-40">
|
||||
<div class="col span-12">
|
||||
<span class="label">
|
||||
Match Regex:
|
||||
{{ t('monitoringRoute.regex.label') }}:
|
||||
</span>
|
||||
<KeyValue
|
||||
v-if="!isView || Object.keys(value.spec.match_re || {}).length > 0"
|
||||
v-model="value.spec.match_re"
|
||||
:disabled="value.isRoot"
|
||||
:options="receiverOptions"
|
||||
:label="t('monitoringRoute.receiver.label')"
|
||||
:mode="mode"
|
||||
:read-allowed="false"
|
||||
add-label="Add match regex"
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export default {
|
|||
:extra-columns="extraColumns"
|
||||
>
|
||||
<template #project-col>
|
||||
<LabeledSelect v-model="project" label="Project" :options="projectOpts" />
|
||||
<LabeledSelect v-model="project" :label="t('namespace.project.label')" :options="projectOpts" />
|
||||
</template>
|
||||
</NameNsDescription>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script>
|
||||
import InputWithSelect from '@/components/form/InputWithSelect';
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||
import Select from '@/components/form/Select';
|
||||
import { get, set } from '@/utils/object';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InputWithSelect, LabeledInput, LabeledSelect
|
||||
InputWithSelect, LabeledInput, Select
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
|
|
@ -53,6 +54,10 @@ export default {
|
|||
return this.serviceTargetStatus === 'warning' ? this.t('ingress.rules.target.doesntExist') : null;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.queueUpdate = debounce(this.update, 500);
|
||||
this.queueUpdatePathTypeAndPath = debounce(this.updatePathTypeAndPath, 500);
|
||||
},
|
||||
methods: {
|
||||
update() {
|
||||
const servicePort = Number.parseInt(this.servicePort) || this.servicePort;
|
||||
|
|
@ -82,14 +87,14 @@ export default {
|
|||
:placeholder="t('ingress.rules.path.placeholder', undefined, true)"
|
||||
:select-value="pathType"
|
||||
:text-value="path"
|
||||
@input="updatePathTypeAndPath"
|
||||
@input="queueUpdatePathTypeAndPath"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="col span-4">
|
||||
<input v-model="path" :placeholder="t('ingress.rules.path.placeholder', undefined, true)" @input="update" />
|
||||
<input v-model="path" :placeholder="t('ingress.rules.path.placeholder', undefined, true)" @input="queueUpdate" />
|
||||
</div>
|
||||
<div class="col" :class="{'span-3': ingress.showPathType, 'span-4': !ingress.showPathType}">
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-model="serviceName"
|
||||
option-label="label"
|
||||
option-key="label"
|
||||
|
|
@ -98,7 +103,7 @@ export default {
|
|||
:taggable="true"
|
||||
:tooltip="serviceTargetTooltip"
|
||||
:hover-tooltip="true"
|
||||
@input="update(); servicePort = ''"
|
||||
@input="queueUpdate(); servicePort = ''"
|
||||
/>
|
||||
</div>
|
||||
<div class="col" :class="{'span-2': ingress.showPathType, 'span-3': !ingress.showPathType}" :style="{'margin-right': '0px'}">
|
||||
|
|
@ -106,14 +111,14 @@ export default {
|
|||
v-if="portOptions.length === 0"
|
||||
v-model="servicePort"
|
||||
:placeholder="t('ingress.rules.port.placeholder')"
|
||||
@input="update"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
<LabeledSelect
|
||||
<Select
|
||||
v-else
|
||||
v-model="servicePort"
|
||||
:options="portOptions"
|
||||
:placeholder="t('ingress.rules.port.placeholder')"
|
||||
@input="update"
|
||||
@input="queueUpdate"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-sm role-link col" @click="$emit('remove')">
|
||||
|
|
@ -122,12 +127,16 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.rule-path {
|
||||
.rule-path ::v-deep {
|
||||
button {
|
||||
line-height: 40px;
|
||||
}
|
||||
input {
|
||||
height: 55px;
|
||||
|
||||
.v-select INPUT {
|
||||
height: 50px;
|
||||
}
|
||||
.labeled-input {
|
||||
padding-top: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export default {
|
|||
<Certificates v-model="value" :mode="mode" :secrets="allSecrets" />
|
||||
</Tab>
|
||||
<Tab
|
||||
v-if="!isView"
|
||||
name="labels-and-annotations"
|
||||
:label="t('generic.labelsAndAnnotations')"
|
||||
:weight="0"
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
import isEmpty from 'lodash/isEmpty';
|
||||
import { RIO } from '@/config/types';
|
||||
import { _VIEW } from '@/config/query-params';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
export default {
|
||||
components: { Select },
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
|
|
@ -122,7 +124,7 @@ export default {
|
|||
<template>
|
||||
<tr>
|
||||
<td>
|
||||
<v-select
|
||||
<Select
|
||||
class="inline"
|
||||
:clearable="false"
|
||||
:value="app"
|
||||
|
|
@ -130,10 +132,10 @@ export default {
|
|||
:placeholder="showPlaceholders ? placeholders[0] : null"
|
||||
:disabled="isView"
|
||||
@input="setApp"
|
||||
></v-select>
|
||||
/>
|
||||
</td>
|
||||
<td v-if="pickVersion">
|
||||
<v-select
|
||||
<Select
|
||||
v-model="version"
|
||||
class="inline"
|
||||
:options="versions"
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@
|
|||
*/
|
||||
|
||||
import LabeledInput from '@/components/form/LabeledInput';
|
||||
import Select from '@/components/form/Select';
|
||||
export default {
|
||||
components: { LabeledInput },
|
||||
components: { LabeledInput, Select },
|
||||
props: {
|
||||
spec: {
|
||||
type: Object,
|
||||
|
|
@ -38,7 +39,10 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
return { all };
|
||||
return {
|
||||
all,
|
||||
ruleOperatorOptions: ['add', 'set', 'remove']
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
format() {
|
||||
|
|
@ -84,7 +88,7 @@ export default {
|
|||
</tr>
|
||||
<tr v-for="(rule, i) in all" :key="i">
|
||||
<td>
|
||||
<v-select v-model="rule.op" :disabled="!enabled" :serachable="false" class="inline" :options="['add', 'set', 'remove']" />
|
||||
<Select v-model="rule.op" :disabled="!enabled" :serachable="false" class="inline" :options="ruleOperatorOptions" />
|
||||
</td>
|
||||
<td>
|
||||
<LabeledInput v-model="rule.name" />
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ import debounce from 'lodash/debounce';
|
|||
import { _EDIT, _VIEW } from '@/config/query-params';
|
||||
import { removeAt, sameContents } from '@/utils/array';
|
||||
import { clone } from '@/utils/object';
|
||||
import Select from '@/components/form/Select';
|
||||
|
||||
const READ_VERBS = ['get', 'list', 'watch'];
|
||||
const WRITE_VERBS = ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'];
|
||||
|
||||
export default {
|
||||
components: { Select },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
|
|
@ -160,7 +162,7 @@ export default {
|
|||
<template v-else>
|
||||
<td class="verbs">
|
||||
<span v-if="isView">{{ row.verbs.join(',') }}</span>
|
||||
<v-select
|
||||
<Select
|
||||
v-else
|
||||
ref="verbs"
|
||||
v-model="row.verbs"
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export default {
|
|||
|
||||
<div class="spacer"></div>
|
||||
<ResourceTabs v-if="!isView" v-model="value" :side-tabs="true" :mode="mode">
|
||||
<Tab name="data" label="Data">
|
||||
<Tab name="data" :label="t('secret.data')">
|
||||
<template v-if="isRegistry">
|
||||
<div id="registry-type" class="row mb-10">
|
||||
<div class="col span-12">
|
||||
|
|
@ -348,7 +348,7 @@ export default {
|
|||
key="data"
|
||||
v-model="value.data"
|
||||
:mode="mode"
|
||||
title="Data"
|
||||
:title="t('secret.data')"
|
||||
:initial-empty-row="true"
|
||||
:value-base64="true"
|
||||
:file-modifier="fileModifier"
|
||||
|
|
|
|||
|
|
@ -142,15 +142,16 @@ export default {
|
|||
}
|
||||
|
||||
return {
|
||||
name: this.value?.metadata?.name || null,
|
||||
spec,
|
||||
type,
|
||||
allConfigMaps: [],
|
||||
allNodes: null,
|
||||
allSecrets: [],
|
||||
allServices: [],
|
||||
name: this.value?.metadata?.name || null,
|
||||
pvcs: [],
|
||||
allNodes: null,
|
||||
showTabs: false,
|
||||
pullPolicyOptions: ['Always', 'IfNotPresent', 'Never'],
|
||||
spec,
|
||||
type,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -640,7 +641,7 @@ export default {
|
|||
<LabeledSelect
|
||||
v-model="container.imagePullPolicy"
|
||||
:label="t('workload.container.imagePullPolicy')"
|
||||
:options="['Always', 'IfNotPresent', 'Never']"
|
||||
:options="pullPolicyOptions"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default {
|
|||
<div>
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect v-model="value.csi.driver" :mode="mode" label="Driver" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
|
||||
<LabeledSelect v-model="value.csi.driver" :mode="mode" :label="t('workload.storage.driver')" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="driverComponent" class="mb-10">
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default {
|
|||
<h3>{{ t('workload.storage.subtypes.csi') }}</h3>
|
||||
<div class="row mb-10">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect v-model="value.csi.driver" :mode="mode" label="Driver" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
|
||||
<LabeledSelect v-model="value.csi.driver" :mode="mode" :label="t('workload.storage.driver')" :options="driverOpts" :get-option-label="opt=>t(`workload.storage.csi.drivers.'${opt}'`)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="value.csi.driver && driverComponent" class="mb-10">
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default {
|
|||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'create'
|
||||
default: 'create',
|
||||
},
|
||||
|
||||
// pod spec
|
||||
|
|
@ -24,23 +24,23 @@ export default {
|
|||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
namespace: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
|
||||
// namespaced configmaps and secrets
|
||||
configMaps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
secrets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
registerBeforeHook: {
|
||||
|
|
@ -66,17 +66,53 @@ export default {
|
|||
},
|
||||
|
||||
opts() {
|
||||
const hasComponent = require.context('@/edit/workload/storage', false, /^.*\.vue$/).keys()
|
||||
const hasComponent = require
|
||||
.context('@/edit/workload/storage', false, /^.*\.vue$/)
|
||||
.keys()
|
||||
.map(path => path.replace(/(\.\/)|(.vue)/g, ''))
|
||||
.filter(file => file !== 'index' && file !== 'Mount' && file !== 'PVC');
|
||||
.filter(
|
||||
file => file !== 'index' && file !== 'Mount' && file !== 'PVC'
|
||||
);
|
||||
|
||||
const out = [...hasComponent, 'csi', 'configMap', 'createPVC', 'persistentVolumeClaim'];
|
||||
const out = [
|
||||
...hasComponent,
|
||||
'csi',
|
||||
'configMap',
|
||||
'createPVC',
|
||||
'persistentVolumeClaim',
|
||||
];
|
||||
|
||||
out.sort();
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
opts2() {
|
||||
const hasComponent = require
|
||||
.context('@/edit/workload/storage', false, /^.*\.vue$/)
|
||||
.keys()
|
||||
.map(path => path.replace(/(\.\/)|(.vue)/g, ''))
|
||||
.filter(
|
||||
file => file !== 'index' && file !== 'Mount' && file !== 'PVC'
|
||||
);
|
||||
|
||||
const out = [
|
||||
...hasComponent,
|
||||
'csi',
|
||||
'configMap',
|
||||
'createPVC',
|
||||
'persistentVolumeClaim',
|
||||
];
|
||||
|
||||
out.sort();
|
||||
|
||||
return out.map(opt => ({
|
||||
label: opt,
|
||||
action: this.addVolume,
|
||||
value: opt,
|
||||
}));
|
||||
},
|
||||
|
||||
pvcNames() {
|
||||
return this.pvcs.map(pvc => pvc.metadata.name);
|
||||
},
|
||||
|
|
@ -97,15 +133,21 @@ export default {
|
|||
addVolume(type) {
|
||||
if (type === 'createPVC') {
|
||||
this.value.volumes.push({
|
||||
_type: 'createPVC', persistentVolumeClaim: {}, name: `vol${ this.value.volumes.length }`
|
||||
_type: 'createPVC',
|
||||
persistentVolumeClaim: {},
|
||||
name: `vol${ this.value.volumes.length }`,
|
||||
});
|
||||
} else if ( type === 'csi' ) {
|
||||
} else if (type === 'csi') {
|
||||
this.value.volumes.push({
|
||||
_type: type, csi: { volumeAttributes: {} }, name: `vol${ this.value.volumes.length }`
|
||||
_type: type,
|
||||
csi: { volumeAttributes: {} },
|
||||
name: `vol${ this.value.volumes.length }`,
|
||||
});
|
||||
} else {
|
||||
this.value.volumes.push({
|
||||
_type: type, [type]: {}, name: `vol${ this.value.volumes.length }`
|
||||
_type: type,
|
||||
[type]: {},
|
||||
name: `vol${ this.value.volumes.length }`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -115,7 +157,9 @@ export default {
|
|||
},
|
||||
|
||||
volumeType(vol) {
|
||||
const type = Object.keys(vol).filter(key => typeof vol[key] === 'object')[0];
|
||||
const type = Object.keys(vol).filter(
|
||||
key => typeof vol[key] === 'object'
|
||||
)[0];
|
||||
|
||||
return type;
|
||||
},
|
||||
|
|
@ -127,7 +171,8 @@ export default {
|
|||
return require(`@/edit/workload/storage/secret.vue`).default;
|
||||
case 'createPVC':
|
||||
case 'persistentVolumeClaim':
|
||||
return require(`@/edit/workload/storage/persistentVolumeClaim/index.vue`).default;
|
||||
return require(`@/edit/workload/storage/persistentVolumeClaim/index.vue`)
|
||||
.default;
|
||||
case 'csi':
|
||||
return require(`@/edit/workload/storage/csi/index.vue`).default;
|
||||
default: {
|
||||
|
|
@ -135,8 +180,7 @@ export default {
|
|||
|
||||
try {
|
||||
component = require(`@/edit/workload/storage/${ type }.vue`).default;
|
||||
} catch {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
|
@ -144,7 +188,9 @@ export default {
|
|||
},
|
||||
|
||||
headerFor(type) {
|
||||
if (this.$store.getters['i18n/exists'](`workload.storage.subtypes.${ type }`)) {
|
||||
if (
|
||||
this.$store.getters['i18n/exists'](`workload.storage.subtypes.${ type }`)
|
||||
) {
|
||||
return this.t(`workload.storage.subtypes.${ type }`);
|
||||
} else {
|
||||
return type;
|
||||
|
|
@ -164,17 +210,16 @@ export default {
|
|||
|
||||
try {
|
||||
button.togglePopover();
|
||||
} catch (e) {
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
// codemirror needs to refresh if it is in a tab that wasn't visible on page load
|
||||
refresh() {
|
||||
if ( this.$refs.cm ) {
|
||||
if (this.$refs.cm) {
|
||||
this.$refs.cm.forEach(component => component.refresh());
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -203,7 +248,7 @@ export default {
|
|||
<CodeMirror
|
||||
ref="cm"
|
||||
:value="yamlDisplay(volume)"
|
||||
:options="{readOnly:true, cursorBlinkRate:-1}"
|
||||
:options="{ readOnly: true, cursorBlinkRate: -1 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -212,27 +257,21 @@ export default {
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col span-6">
|
||||
<ButtonDropdown v-if="mode!=='view'" ref="buttonDropdown" size="sm">
|
||||
<template #button-content>
|
||||
<button v-if="mode!=='view'" type="button" class="btn btn-sm text-primary bg-transparent" @click="openPopover">
|
||||
{{ t('workload.storage.addVolume') }}
|
||||
</button>
|
||||
</template>
|
||||
<template #popover-content>
|
||||
<ul class="list-unstyled menu">
|
||||
<li v-for="opt in opts" :key="opt" v-close-popover @click="addVolume(opt)">
|
||||
{{ t(`workload.storage.subtypes.${opt}`) }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ButtonDropdown>
|
||||
<ButtonDropdown
|
||||
:button-label="t('workload.storage.addVolume')"
|
||||
:dropdown-options="opts"
|
||||
size="sm"
|
||||
@click-action="addVolume"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.volume-source{
|
||||
.volume-source {
|
||||
padding: 20px;
|
||||
margin: 20px 0px 20px 0px;
|
||||
position: relative;
|
||||
|
||||
::v-deep .code-mirror {
|
||||
|
|
@ -249,12 +288,11 @@ export default {
|
|||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding:0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.add-vol:focus{
|
||||
.add-vol:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -54,10 +54,23 @@ export default {
|
|||
notView() {
|
||||
return (this.mode !== _VIEW);
|
||||
},
|
||||
|
||||
isSearchable() {
|
||||
const { searchable } = this;
|
||||
const options = ( this.options || [] );
|
||||
|
||||
if (searchable || options.length >= 10) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onFocus() {
|
||||
this.$emit('on-focus');
|
||||
|
||||
return this.onFocusLabeled();
|
||||
},
|
||||
|
||||
|
|
@ -67,6 +80,8 @@ export default {
|
|||
},
|
||||
|
||||
onBlur() {
|
||||
this.$emit('on-blur');
|
||||
|
||||
return this.onBlurLabeled();
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
mappedKeys(map, vm) {
|
||||
// Defaults found at - https://github.com/sagalbot/vue-select/blob/master/src/components/Select.vue#L947
|
||||
const out = { ...map };
|
||||
|
||||
// tab
|
||||
(out[9] = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const optsLen = vm.filteredOptions.length;
|
||||
const typeAheadPointer = vm.typeAheadPointer;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (typeAheadPointer === 0) {
|
||||
vm.$refs.search.focus();
|
||||
|
||||
return vm.onEscape();
|
||||
}
|
||||
|
||||
return vm.typeAheadUp();
|
||||
}
|
||||
if (typeAheadPointer + 1 === optsLen) {
|
||||
$(vm.$el).next().focus();
|
||||
|
||||
return vm.onEscape();
|
||||
}
|
||||
|
||||
return vm.typeAheadDown();
|
||||
});
|
||||
|
||||
return out;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { CIS } from '@/config/types';
|
||||
import { downloadFile } from '@/utils/download';
|
||||
import { findBy } from '@/utils/array';
|
||||
import { downloadFile, generateZip } from '@/utils/download';
|
||||
import { isEmpty, set } from '@/utils/object';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
|
||||
export default {
|
||||
_availableActions() {
|
||||
this.getReport();
|
||||
let out = this._standardActions;
|
||||
|
||||
const toFilter = ['cloneYaml', 'goToEditYaml', 'download'];
|
||||
|
|
@ -14,36 +16,67 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
const t = this.$rootGetters['i18n/t'];
|
||||
|
||||
const downloadReport = {
|
||||
action: 'downloadReport',
|
||||
action: 'downloadLatestReport',
|
||||
enabled: this.hasReport,
|
||||
icon: 'icon icon-fw icon-chevron-right',
|
||||
label: 'Download Report',
|
||||
icon: 'icon icon-fw icon-download',
|
||||
label: t('cis.downloadLatestReport'),
|
||||
total: 1,
|
||||
};
|
||||
|
||||
const downloadAllReports = {
|
||||
action: 'downloadAllReports',
|
||||
enabled: this.hasReport,
|
||||
icon: 'icon icon-fw icon-download',
|
||||
label: t('cis.downloadAllReports'),
|
||||
total: 1,
|
||||
};
|
||||
|
||||
if (this.hasReports) {
|
||||
out.unshift({ divider: true });
|
||||
if (this.spec?.cronSchedule) {
|
||||
out.unshift(downloadAllReports);
|
||||
}
|
||||
out.unshift(downloadReport);
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
hasReport: false,
|
||||
applyDefaults() {
|
||||
return () => {
|
||||
const spec = this.spec || {};
|
||||
|
||||
getReport() {
|
||||
return async() => {
|
||||
const owned = await this.getOwned();
|
||||
const reportCRD = owned.filter(each => each.type === CIS.REPORT)[0];
|
||||
|
||||
this.hasReport = !!reportCRD;
|
||||
|
||||
return reportCRD;
|
||||
spec.scanProfileName = null;
|
||||
spec.scanAlertRule = {};
|
||||
spec.scoreWarning = 'pass';
|
||||
set(this, 'spec', spec);
|
||||
};
|
||||
},
|
||||
|
||||
downloadReport() {
|
||||
hasReports() {
|
||||
const { relationships = [] } = this.metadata;
|
||||
|
||||
const reportRel = findBy(relationships, 'toType', CIS.REPORT);
|
||||
|
||||
return !!reportRel;
|
||||
},
|
||||
|
||||
getReports() {
|
||||
return async() => {
|
||||
const report = await this.getReport();
|
||||
const owned = await this.getOwned();
|
||||
const reportCRDs = owned.filter(each => each.type === CIS.REPORT);
|
||||
|
||||
return reportCRDs;
|
||||
};
|
||||
},
|
||||
|
||||
downloadLatestReport() {
|
||||
return async() => {
|
||||
const reports = await this.getReports() || [];
|
||||
const report = sortBy(reports, 'metadata.creationTimestamp', true)[0];
|
||||
const Papa = await import(/* webpackChunkName: "cis" */'papaparse');
|
||||
|
||||
try {
|
||||
|
|
@ -56,4 +89,29 @@ export default {
|
|||
}
|
||||
};
|
||||
},
|
||||
|
||||
downloadAllReports() {
|
||||
return async() => {
|
||||
const toZip = {};
|
||||
const reports = await this.getReports() || [];
|
||||
const Papa = await import(/* webpackChunkName: "cis" */'papaparse');
|
||||
|
||||
reports.forEach((report) => {
|
||||
try {
|
||||
const testResults = report.aggregatedTests;
|
||||
const csv = Papa.unparse(testResults);
|
||||
|
||||
toZip[`${ report.id }.csv`] = csv;
|
||||
} catch (err) {
|
||||
this.$dispatch('growl/fromError', { title: 'Error downloading file', err }, { root: true });
|
||||
}
|
||||
});
|
||||
if (!isEmpty(toZip)) {
|
||||
generateZip(toZip).then((zip) => {
|
||||
downloadFile(`${ this.id }-reports`, zip, 'application/zip');
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
export default {
|
||||
warnDeletionMessage() {
|
||||
return (toRemove = []) => {
|
||||
return this.$rootGetters['i18n/t']('cis.deleteProfileWarning', { count: toRemove.length });
|
||||
};
|
||||
},
|
||||
|
||||
numberTestsSkipped() {
|
||||
const { skipTests = [] } = this.spec;
|
||||
|
||||
return skipTests.length;
|
||||
}
|
||||
};
|
||||
|
|
@ -803,7 +803,7 @@ export default {
|
|||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledSelect
|
||||
label="Chart"
|
||||
:label="t('catalog.install.chart')"
|
||||
:value="chart"
|
||||
:options="charts"
|
||||
:get-option-label="opt => getOptionLabel(opt)"
|
||||
|
|
@ -813,7 +813,7 @@ export default {
|
|||
</div>
|
||||
<div v-if="chart" class="col span-6">
|
||||
<LabeledSelect
|
||||
label="Version"
|
||||
:label="t('catalog.install.version')"
|
||||
:value="$route.query.version"
|
||||
option-label="version"
|
||||
option-key="version"
|
||||
|
|
|
|||
|
|
@ -779,7 +779,7 @@ export const getters = {
|
|||
};
|
||||
},
|
||||
|
||||
headersFor(state) {
|
||||
headersFor(state, getters, rootState, rootGetters) {
|
||||
return (schema) => {
|
||||
const attributes = schema.attributes || {};
|
||||
const columns = attributes.columns || [];
|
||||
|
|
@ -790,7 +790,7 @@ export const getters = {
|
|||
if ( typeof entry === 'string' ) {
|
||||
const col = findBy(columns, 'name', entry);
|
||||
if ( col ) {
|
||||
return fromSchema(col);
|
||||
return fromSchema(col, rootGetters);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -812,7 +812,7 @@ export const getters = {
|
|||
out.push(NAMESPACE);
|
||||
}
|
||||
} else {
|
||||
out.push(fromSchema(col));
|
||||
out.push(fromSchema(col, rootGetters));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -831,7 +831,7 @@ export const getters = {
|
|||
|
||||
return out;
|
||||
|
||||
function fromSchema(col) {
|
||||
function fromSchema(col, rootGetters) {
|
||||
let formatter, width, formatterOpts;
|
||||
|
||||
if ( (col.format === '' || col.format == 'date') && col.name === 'Age' ) {
|
||||
|
|
@ -848,9 +848,13 @@ export const getters = {
|
|||
formatter = 'Number';
|
||||
}
|
||||
|
||||
const exists = rootGetters['i18n/exists']
|
||||
const t = rootGetters['i18n/t']
|
||||
const labelKey = `tableHeaders.${col.name}`
|
||||
|
||||
return {
|
||||
name: col.name.toLowerCase(),
|
||||
label: col.name,
|
||||
label: exists(labelKey) ? t(labelKey) : col.name,
|
||||
value: col.field.startsWith('.') ? `$${ col.field }` : col.field,
|
||||
sort: [col.field],
|
||||
formatter,
|
||||
|
|
|
|||
|
|
@ -7,15 +7,13 @@ export async function downloadFile(fileName, content, contentType = 'text/plain;
|
|||
return saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
// [{name: 'file1', file: 'data'}, {name: 'file2', file: 'data2'}]
|
||||
// {[fileName1]:data1, [fileName2]:data2}
|
||||
export function generateZip(files) {
|
||||
// Moving this to a dynamic const JSZip = import('jszip') didn't work... figure out later
|
||||
const zip = new JSZip();
|
||||
|
||||
for ( const fileName in files) {
|
||||
const file = files[fileName];
|
||||
|
||||
zip.file(fileName, file.data);
|
||||
zip.file(fileName, files[fileName]);
|
||||
}
|
||||
|
||||
return zip.generateAsync({ type: 'blob' }).then((contents) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue