mirror of https://github.com/rancher/dashboard.git
Merge remote-tracking branch 'origin/update-user-password-2' into api-keys
This commit is contained in:
commit
2a117c8c10
|
|
@ -75,9 +75,7 @@ BUTTON,
|
||||||
.checkbox-custom,
|
.checkbox-custom,
|
||||||
.radio-custom {
|
.radio-custom {
|
||||||
&:focus, &.focused {
|
&:focus, &.focused {
|
||||||
outline: none;
|
@include form-focus }
|
||||||
box-shadow: 0 0 0 var(--outline-width) var(--outline);
|
|
||||||
background: var(--input-focus-bg) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
A {
|
A {
|
||||||
|
|
|
||||||
|
|
@ -156,3 +156,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin form-focus {
|
||||||
|
// Focus for form like elements (not to be confused with basic :focus style)
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 var(--outline-width) var(--outline);
|
||||||
|
background: var(--input-focus-bg)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.link-container {
|
.link-container {
|
||||||
|
position: relative;
|
||||||
background-color: var(--input-bg);
|
background-color: var(--input-bg);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border: solid 1px var(--input-border);
|
border: solid 1px var(--input-border);
|
||||||
|
|
@ -25,6 +26,20 @@
|
||||||
> * {
|
> * {
|
||||||
opacity: .3;
|
opacity: .3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-msg{
|
||||||
|
position:absolute;
|
||||||
|
color: var(--error);
|
||||||
|
z-index: z-index('hoverOverContent');
|
||||||
|
opacity: 1;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.disabled) {
|
&:hover:not(.disabled) {
|
||||||
|
|
|
||||||
|
|
@ -138,49 +138,146 @@ account:
|
||||||
|
|
||||||
authConfig:
|
authConfig:
|
||||||
accessMode:
|
accessMode:
|
||||||
label: Configure who should be able to login and use {vendor}
|
label: 'Configure who should be able to login and use {vendor}'
|
||||||
unrestricted: "Allow any valid user"
|
required: Restrict access to only the auhorized users & groups
|
||||||
restricted: "Allow members of clusters and projects, plus authorized users & groups"
|
restricted: 'Allow members of clusters and projects, plus authorized users & groups'
|
||||||
required: "Restrict access to only the auhorized users & groups"
|
unrestricted: Allow any valid user
|
||||||
allowedPrincipalIds:
|
allowedPrincipalIds:
|
||||||
title: Authorized Users & Groups
|
title: Authorized Users & Groups
|
||||||
associatedWarning: "Note: The {provider} user you authenticate as will be associated as an alternate way to login to the {vendor} user you are currently logged in as (<code>{username}</code>)."
|
associatedWarning: 'Note: The {provider} user you authenticate as will be associated as an alternate way to login to the {vendor} user you are currently logged in as (<code>{username}</code>).'
|
||||||
stateBanner:
|
|
||||||
enabled: "{provider} is currently enabled."
|
|
||||||
disabled: "{provider} is currently disabled."
|
|
||||||
github:
|
github:
|
||||||
target:
|
|
||||||
label: Which version of GitHub do you want to use?
|
|
||||||
public: Public GitHub.com
|
|
||||||
private: A private installation of GitHub Enterprise
|
|
||||||
host:
|
|
||||||
label: GitHub Enterprise Host
|
|
||||||
placeholder: e.g. github.mycompany.example
|
|
||||||
clientId:
|
clientId:
|
||||||
label: Client ID
|
label: Client ID
|
||||||
clientSecret:
|
clientSecret:
|
||||||
label: Client Secret
|
label: Client Secret
|
||||||
form:
|
form:
|
||||||
|
app:
|
||||||
|
label: Application name
|
||||||
|
value: 'Anything you like, e.g. My {vendor}'
|
||||||
|
calllback:
|
||||||
|
label: Authorization callback URL
|
||||||
|
description:
|
||||||
|
label: Description
|
||||||
|
value: 'Optional, can be left blank'
|
||||||
|
homepage:
|
||||||
|
label: Homepage URL
|
||||||
|
instruction: 'Fill in the form with these values:'
|
||||||
prefix: |-
|
prefix: |-
|
||||||
<li><a href="{baseUrl}/settings/developers" target="_blank" rel="noopener noreferrer nofollow">Click here</a> to go to GitHub application settings in a new window.</li>
|
<li><a href="{baseUrl}/settings/developers" target="_blank" rel="noopener noreferrer nofollow">Click here</a> to go to GitHub application settings in a new window.</li>
|
||||||
<li>Click on the "OAuth Apps" tab.</li>
|
<li>Click on the "OAuth Apps" tab.</li>
|
||||||
<li>Click the "New OAuth App" button.</li>
|
<li>Click the "New OAuth App" button.</li>
|
||||||
instruction: "Fill in the form with these values:"
|
|
||||||
app:
|
|
||||||
label: Application name
|
|
||||||
value: Anything you like, e.g. My {vendor}
|
|
||||||
homepage:
|
|
||||||
label: Homepage URL
|
|
||||||
description:
|
|
||||||
label: Description
|
|
||||||
value: Optional, can be left blank
|
|
||||||
calllback:
|
|
||||||
label: Authorization callback URL
|
|
||||||
suffix: |-
|
suffix: |-
|
||||||
<li>Click "Register application"</li>
|
<li>Click "Register application"</li>
|
||||||
<li>Copy and paste the Client ID and Client Secret of your newly created OAuth app into the fields below</li>
|
<li>Copy and paste the Client ID and Client Secret of your newly created OAuth app into the fields below</li>
|
||||||
|
host:
|
||||||
|
label: GitHub Enterprise Host
|
||||||
|
placeholder: e.g. github.mycompany.example
|
||||||
|
target:
|
||||||
|
label: Which version of GitHub do you want to use?
|
||||||
|
private: A private installation of GitHub Enterprise
|
||||||
|
public: Public GitHub.com
|
||||||
|
googleoauth:
|
||||||
|
adminEmail: Admin Email
|
||||||
|
domain: Domain
|
||||||
|
oauthCredentials:
|
||||||
|
label: OAuth Credentials
|
||||||
|
tip: The OAuth Credentials JSON can be found in the Google API developers console.
|
||||||
|
serviceAccountCredentials:
|
||||||
|
label: Service Account Credentials
|
||||||
|
tip: The Service Account Credentials JSON can be found in the service accounts section of the Google API developers console.
|
||||||
|
steps:
|
||||||
|
1:
|
||||||
|
title: 'Step One: For standard Google, click <a href="https://console.developers.google.com/apis/credentials" target="_blank" rel="noopener noreferrer nofollow">here</a> to go applications settings in a new window'
|
||||||
|
body: |-
|
||||||
|
<ul class="mt-0">
|
||||||
|
<li>Login to your account. Navigate to "APIs & Services" and then select "OAuth consent screen". </li>
|
||||||
|
<li>Authorized domains: Top private domain of {hostname} </li>
|
||||||
|
<li>Application homepage link: {serverUrl}</li>
|
||||||
|
<li>Under Scopes for Google APIs, enable "email", "profile", and "openid". </li>
|
||||||
|
<li>Click on "Save". </li>
|
||||||
|
</ul>
|
||||||
|
2:
|
||||||
|
title: 'Step Two: Navigate to the "Credentials" tab to create your OAuth client ID'
|
||||||
|
body: |-
|
||||||
|
<ul class="mt-0">
|
||||||
|
<li>Select the "Create Credentials" dropdown, and select "OAuth clientID", then select "Web application".</li>
|
||||||
|
<li>Authorized Javascript origins: {serverUrl}</li>
|
||||||
|
<li>Authorized redirect URIs: {serverUrl}/verify?test </li>
|
||||||
|
<li>Click "Create", and then click on the "Download JSON" button.</li>
|
||||||
|
<li>Upload the downloaded JSON file in the OAuth credentials box.</li>
|
||||||
|
</ul>
|
||||||
|
3:
|
||||||
|
title: 'Step Three: Create Service Account credentials'
|
||||||
|
body: |-
|
||||||
|
Follow <a href="https://rancher.com/docs/rancher/v2.x/en/admin-settings/authentication/google/#creating-service-account-credentials" target="_blank" rel="noopener noreferrer nofollow">this</a> guide to:<br/>
|
||||||
|
<ul class="mt-0">
|
||||||
|
<li> Create a service account.</li>
|
||||||
|
<li> Generate a key for the service account.</li>
|
||||||
|
<li> Add the service account as an OAuth client in your google domain.</li>
|
||||||
|
</ul>
|
||||||
|
ldap:
|
||||||
|
freeipa: Configure a FreeIPA server
|
||||||
|
activedirectory: Configure an Active Directory account
|
||||||
|
openldap: Configure an OpenLDAP server
|
||||||
|
defaultLoginDomain: Default Login Domain
|
||||||
|
cert: Certificate
|
||||||
|
disabledStatusBitmask: Disabled Status Bitmask
|
||||||
|
groupDNAttribute: Group DN Attribute
|
||||||
|
groupMemberMappingAttribute: Group Member Mapping Attribute
|
||||||
|
groupMemberUserAttribute: Group Member User Attribute
|
||||||
|
groupSearchBase:
|
||||||
|
label: Group Search Base
|
||||||
|
placeholder: 'ou=groups,dc=mycompany,dc=com'
|
||||||
|
hostname: Hostname/IP
|
||||||
|
loginAttribute: Login Attribute
|
||||||
|
nameAttribute: Name Attribute
|
||||||
|
nestedGroupMembership:
|
||||||
|
label: Nested Group Membership
|
||||||
|
options:
|
||||||
|
direct: Search only direct group memberships
|
||||||
|
nested: Search direct and nested group memberships
|
||||||
|
objectClass: Object Class
|
||||||
|
password: Password
|
||||||
|
port: Port
|
||||||
|
customizeSchema: Customize Schema
|
||||||
|
users: Users
|
||||||
|
groups: Groups
|
||||||
|
searchAttribute: Search Attribute
|
||||||
|
searchFilter: Search Filter
|
||||||
|
serverConnectionTimeout: Server Connection Timeout
|
||||||
|
serviceAccountDN: Service Account Distinguished Name
|
||||||
|
serviceAccountPassword: Service Account Password
|
||||||
|
serviceAccountInfo: Rancher needs a service account that has read-only access to all of the domains that will be able to login, so that we can determine what groups a user is a member of when they make a request with an API key.
|
||||||
|
starttls:
|
||||||
|
label: Start TLS
|
||||||
|
tip: Upgrades non-encrypted connections by wrapping with TLS during the connection process. Can not be used in conjunction with TLS.
|
||||||
|
tls: TLS
|
||||||
|
userEnabledAttribute: User Enabled Attribute
|
||||||
|
userMemberAttribute: User Member Attribute
|
||||||
|
userSearchBase:
|
||||||
|
label: User Search Base
|
||||||
|
placeholder: 'e.g. ou=users,dc=mycompany,dc=com'
|
||||||
|
username: Username
|
||||||
|
usernameAttribute: Username Attribute
|
||||||
|
saml:
|
||||||
|
UID: UID Field
|
||||||
|
adfs: Configure an AD FS account
|
||||||
|
api: Rancher API Host
|
||||||
|
cert: Certificate
|
||||||
|
displayName: Display Name Field
|
||||||
|
groups: Groups Field
|
||||||
|
key: Private Key
|
||||||
|
keycloak: Configure a Keycloak account
|
||||||
|
metadata: Metadata XML
|
||||||
|
okta: Configure an Okta account
|
||||||
|
ping: Configure a Ping account
|
||||||
|
shibboleth: Congiure a Shibboleth account
|
||||||
|
showLdap: Configure an OpenLDAP Server
|
||||||
|
userName: User Name Field
|
||||||
|
stateBanner:
|
||||||
|
disabled: '{provider} is currently disabled.'
|
||||||
|
enabled: '{provider} is currently enabled.'
|
||||||
|
testAndEnable: Test and Enable Authentication
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -263,48 +360,45 @@ asyncButton:
|
||||||
|
|
||||||
backupRestoreOperator:
|
backupRestoreOperator:
|
||||||
backupFilename: Backup Filename
|
backupFilename: Backup Filename
|
||||||
deployment:
|
|
||||||
rancherNamespace: Rancher ResourceSet Namespace
|
|
||||||
storage:
|
|
||||||
tip: Configure a storage location where all backups are saved by default. You will have the option to override this with each backup, but will be limited to using an S3-compatible object store.
|
|
||||||
storageClass:
|
|
||||||
label: Storage Class
|
|
||||||
persistentVolume:
|
|
||||||
label: Persistent Volume
|
|
||||||
label: Default Storage Location
|
|
||||||
options:
|
|
||||||
none: No default storage location
|
|
||||||
s3: Use an S3-compatible object store
|
|
||||||
defaultStorageClass: 'Use the default storage class ({name})'
|
|
||||||
pickSC: Use an existing storage class
|
|
||||||
pickPV: Use an existing persistent volume
|
|
||||||
warning: This {type} does not have its reclaim policy set to "Retain". Your backups may be lost if the volume is changed or becomes unbound.
|
|
||||||
size: Size
|
|
||||||
prune:
|
|
||||||
label: Prune
|
|
||||||
tip: Delete the resources managed by Rancher that are not present in the backup. (Recommended)
|
|
||||||
encryption: Encryption
|
|
||||||
encryptionConfigName:
|
|
||||||
label: Encryption Config Secret
|
|
||||||
backuptip: Any secret in the <code>cattle-resource-system</code> namespace that has an <code>encryption-provider-config.yaml</code> key. <br/>The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.
|
|
||||||
restoretip: If the backup was performed with encryption enabled, a secret containing the same encryption-provider-config should be used during restore.
|
|
||||||
options:
|
|
||||||
none: Store the contents of the backup unencrypted
|
|
||||||
secret: Encrypt backups using an <a target="_blank" rel="noopener noreferrer nofollow" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration">Encryption Config Secret</a> (Recommended)
|
|
||||||
warning: The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.
|
|
||||||
deleteTimeout:
|
deleteTimeout:
|
||||||
label: Delete Timeout
|
label: Delete Timeout
|
||||||
tip: Seconds to wait for a resource delete to succeed before removing finalizers to force deletion.
|
tip: Seconds to wait for a resource delete to succeed before removing finalizers to force deletion.
|
||||||
resourceSetName: Resource Set
|
deployment:
|
||||||
schedule:
|
rancherNamespace: Rancher ResourceSet Namespace
|
||||||
label: Schedule
|
size: Size
|
||||||
placeholder: e.g. @midnight or 0 0 * * *
|
storage:
|
||||||
|
label: Default Storage Location
|
||||||
|
options:
|
||||||
|
defaultStorageClass: 'Use the default storage class ({name})'
|
||||||
|
none: No default storage location
|
||||||
|
pickPV: Use an existing persistent volume
|
||||||
|
pickSC: Use an existing storage class
|
||||||
|
s3: Use an S3-compatible object store
|
||||||
|
persistentVolume:
|
||||||
|
label: Persistent Volume
|
||||||
|
storageClass:
|
||||||
|
label: Storage Class
|
||||||
|
tip: 'Configure a storage location where all backups are saved by default. You will have the option to override this with each backup, but will be limited to using an S3-compatible object store.'
|
||||||
|
warning: 'This {type} does not have its reclaim policy set to "Retain". Your backups may be lost if the volume is changed or becomes unbound.'
|
||||||
|
encryption: Encryption
|
||||||
|
encryptionConfigName:
|
||||||
|
backuptip: 'Any secret in the <code>cattle-resource-system</code> namespace that has an <code>encryption-provider-config.yaml</code> key. <br/>The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'
|
||||||
|
label: Encryption Config Secret
|
||||||
options:
|
options:
|
||||||
disabled: One-Time Backup
|
none: Store the contents of the backup unencrypted
|
||||||
enabled: Recurring Backups
|
secret: 'Encrypt backups using an <a target="_blank" rel="noopener noreferrer nofollow" href="https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration">Encryption Config Secret</a> (Recommended)'
|
||||||
|
restoretip: 'If the backup was performed with encryption enabled, a secret containing the same encryption-provider-config should be used during restore.'
|
||||||
|
warning: 'The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'
|
||||||
|
lastBackup: Last Backup
|
||||||
|
nextBackup: Next Backup
|
||||||
|
noResourceSet: You must define a ResourceSet in this namespace to create a backup CR.
|
||||||
|
prune:
|
||||||
|
label: Prune
|
||||||
|
tip: Delete the resources managed by Rancher that are not present in the backup. (Recommended)
|
||||||
|
resourceSetName: Resource Set
|
||||||
restoreFrom:
|
restoreFrom:
|
||||||
existing: An existing backup config
|
|
||||||
default: The default storage target
|
default: The default storage target
|
||||||
|
existing: An existing backup config
|
||||||
s3: An S3-compatible object store
|
s3: An S3-compatible object store
|
||||||
retentionCount:
|
retentionCount:
|
||||||
label: Retention Count
|
label: Retention Count
|
||||||
|
|
@ -314,24 +408,30 @@ backupRestoreOperator:
|
||||||
other { Files }
|
other { Files }
|
||||||
}
|
}
|
||||||
s3:
|
s3:
|
||||||
|
bucketName: Bucket Name
|
||||||
|
credentialSecretName: Credential Secret
|
||||||
|
endpoint: Endpoint
|
||||||
|
endpointCA: Endpoint CA
|
||||||
|
folder: Folder
|
||||||
|
insecureTLSSkipVerify: Skip TLS Verifications
|
||||||
|
region: Region
|
||||||
|
storageLocation: Storage Location
|
||||||
titles:
|
titles:
|
||||||
backupLocation: Backup Source
|
backupLocation: Backup Source
|
||||||
location: Storage Location
|
location: Storage Location
|
||||||
s3: S3
|
s3: S3
|
||||||
credentialSecretName: Credential Secret
|
schedule:
|
||||||
storageLocation: Storage Location
|
label: Schedule
|
||||||
endpoint: Endpoint
|
options:
|
||||||
endpointCA: Endpoint CA
|
disabled: One-Time Backup
|
||||||
bucketName: Bucket Name
|
enabled: Recurring Backups
|
||||||
region: Region
|
placeholder: e.g. @midnight or 0 0 * * *
|
||||||
folder: Folder
|
|
||||||
insecureTLSSkipVerify: Skip TLS Verifications
|
|
||||||
storageSource:
|
storageSource:
|
||||||
useDefault: Use the default storage location configured during installation
|
|
||||||
configureS3: Use an S3-compatible object store
|
configureS3: Use an S3-compatible object store
|
||||||
useBackup: Use the s3 location specified on the Backup CR
|
useBackup: Use the s3 location specified on the Backup CR
|
||||||
|
useDefault: Use the default storage location configured during installation
|
||||||
targetBackup: Target Backup
|
targetBackup: Target Backup
|
||||||
noResourceSet: 'You must define a ResourceSet in this namespace to create a backup CR.'
|
|
||||||
|
|
||||||
|
|
||||||
catalog:
|
catalog:
|
||||||
|
|
@ -807,6 +907,7 @@ istio:
|
||||||
jaeger:
|
jaeger:
|
||||||
label: Jaeger
|
label: Jaeger
|
||||||
description: Monitor and Troubleshoot microservices-based distributed systems.
|
description: Monitor and Troubleshoot microservices-based distributed systems.
|
||||||
|
disabled: '{app} is not installed'
|
||||||
cni: Enabled CNI
|
cni: Enabled CNI
|
||||||
customOverlayFile:
|
customOverlayFile:
|
||||||
label: Custom Overlay File
|
label: Custom Overlay File
|
||||||
|
|
@ -1089,7 +1190,7 @@ monitoring:
|
||||||
className: Storage Class Name
|
className: Storage Class Name
|
||||||
existingClaim: Use Existing Claim
|
existingClaim: Use Existing Claim
|
||||||
finalizers: PVC Finalizers
|
finalizers: PVC Finalizers
|
||||||
label: Persistent Storage for Grafana
|
label: Grafana Storage
|
||||||
mode: Access Mode
|
mode: Access Mode
|
||||||
selector: Selector
|
selector: Selector
|
||||||
size: Size
|
size: Size
|
||||||
|
|
@ -1250,6 +1351,8 @@ namespace:
|
||||||
project:
|
project:
|
||||||
label: Project
|
label: Project
|
||||||
resources: Resources
|
resources: Resources
|
||||||
|
enableAutoInjection: Enable Istio Auto Injection
|
||||||
|
disableAutoInjection: Disable Istio Auto Injection
|
||||||
|
|
||||||
namespaceFilter:
|
namespaceFilter:
|
||||||
selected:
|
selected:
|
||||||
|
|
@ -1340,6 +1443,35 @@ prefs:
|
||||||
hideDesc:
|
hideDesc:
|
||||||
label: Hide All Type Description Boxes
|
label: Hide All Type Description Boxes
|
||||||
|
|
||||||
|
accountAndKeys:
|
||||||
|
title: Account and API Keys
|
||||||
|
account:
|
||||||
|
title: Account
|
||||||
|
name: Name
|
||||||
|
username: Username
|
||||||
|
change: Change Password
|
||||||
|
keys:
|
||||||
|
title: API Keys
|
||||||
|
|
||||||
|
changePassword:
|
||||||
|
title: Change Password
|
||||||
|
cancel: Cancel
|
||||||
|
keys: Delete all existing API keys
|
||||||
|
generatePassword: Generate a random password
|
||||||
|
newGeneratedPassword: Suggest a password
|
||||||
|
currentPassword: Current Password
|
||||||
|
userGen:
|
||||||
|
newPassword: New Password
|
||||||
|
confirmPassword: Confirm Password
|
||||||
|
randomGen:
|
||||||
|
copy: Copy to Clipboard
|
||||||
|
generated: Generated Password
|
||||||
|
errors:
|
||||||
|
missmatchedPassword: Passwords do not match
|
||||||
|
failedToChange: Failed to change password
|
||||||
|
failedDeleteKey: Failed to delete key
|
||||||
|
failedDeleteKeys: Failed to delete keys
|
||||||
|
|
||||||
principal:
|
principal:
|
||||||
loading: Loading…
|
loading: Loading…
|
||||||
error: Unable to fetch principal info
|
error: Unable to fetch principal info
|
||||||
|
|
@ -1639,8 +1771,15 @@ servicesPage:
|
||||||
ports:
|
ports:
|
||||||
label: Ports
|
label: Ports
|
||||||
selectors:
|
selectors:
|
||||||
helpText: "If no selector is created, manual endpoints must be made."
|
helpText: ""
|
||||||
label: Selectors
|
label: Selectors
|
||||||
|
matchingPods:
|
||||||
|
matchesSome: |-
|
||||||
|
{matched, plural,
|
||||||
|
=0 {Matches 0 of {total, number} pods. If no selector is created, manual endpoints must be made.}
|
||||||
|
=1 {Matches 1 of {total, number} pods: "{sample}"}
|
||||||
|
other {Matches {matched, number} of {total, number} existing pods, including "{sample}"}
|
||||||
|
}
|
||||||
serviceTypes:
|
serviceTypes:
|
||||||
clusterIp:
|
clusterIp:
|
||||||
abbrv: IP
|
abbrv: IP
|
||||||
|
|
@ -2592,6 +2731,9 @@ action:
|
||||||
view: View Config
|
view: View Config
|
||||||
viewInApi: View in API
|
viewInApi: View in API
|
||||||
viewYaml: View YAML
|
viewYaml: View YAML
|
||||||
|
show: Show
|
||||||
|
hide: Hide
|
||||||
|
copy: Copy
|
||||||
|
|
||||||
unit:
|
unit:
|
||||||
sec: secs
|
sec: secs
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,6 @@ export default {
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
storageSource(neu) {
|
storageSource(neu) {
|
||||||
this.reclaimWarning = false;
|
|
||||||
switch (neu) {
|
switch (neu) {
|
||||||
case 'pickSC':
|
case 'pickSC':
|
||||||
this.value.persistence.enabled = true;
|
this.value.persistence.enabled = true;
|
||||||
|
|
@ -107,12 +106,16 @@ export default {
|
||||||
this.value.persistence.storageClass = this.defaultStorageClass?.id;
|
this.value.persistence.storageClass = this.defaultStorageClass?.id;
|
||||||
this.storageClass = this.defaultStorageClass;
|
this.storageClass = this.defaultStorageClass;
|
||||||
}
|
}
|
||||||
|
if (this.storageClass?.reclaimPolicy === 'Delete') {
|
||||||
|
this.reclaimWarning = true;
|
||||||
|
}
|
||||||
delete this.value.persistence.volumeName;
|
delete this.value.persistence.volumeName;
|
||||||
break;
|
break;
|
||||||
case 'pickPV':
|
case 'pickPV':
|
||||||
this.value.persistence.enabled = true;
|
this.value.persistence.enabled = true;
|
||||||
this.value.s3.enabled = false;
|
this.value.s3.enabled = false;
|
||||||
this.value.persistence.storageClass = '-';
|
this.value.persistence.storageClass = '-';
|
||||||
|
this.reclaimWarning = false;
|
||||||
break;
|
break;
|
||||||
case 's3':
|
case 's3':
|
||||||
this.value.persistence.enabled = false;
|
this.value.persistence.enabled = false;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import ChangePassword from '@/components/form/ChangePassword';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import AsyncButton from '@/components/AsyncButton';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Card, AsyncButton, ChangePassword
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { valid: false, password: '' };
|
||||||
|
},
|
||||||
|
computed: { ...mapGetters({ t: 'i18n/t' }) },
|
||||||
|
methods: {
|
||||||
|
show(show) {
|
||||||
|
if (show) {
|
||||||
|
this.$modal.show('password-modal');
|
||||||
|
} else {
|
||||||
|
this.$modal.hide('password-modal');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submit(buttonCb) {
|
||||||
|
try {
|
||||||
|
await this.$refs.changePassword.submit();
|
||||||
|
this.show(false);
|
||||||
|
buttonCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
buttonCb(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
class="change-password-modal"
|
||||||
|
name="password-modal"
|
||||||
|
:width="500"
|
||||||
|
:height="445"
|
||||||
|
>
|
||||||
|
<Card class="prompt-password" :show-highlight-border="false">
|
||||||
|
<h4 slot="title" class="text-default-text">
|
||||||
|
{{ t("changePassword.title") }}
|
||||||
|
</h4>
|
||||||
|
<div slot="body">
|
||||||
|
<form @submit.prevent>
|
||||||
|
<ChangePassword ref="changePassword" v-model="password" @valid="valid = $event" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #actions>
|
||||||
|
<!-- type reset is required by lastpass -->
|
||||||
|
<button class="btn role-secondary" type="reset" @click="show(false)">
|
||||||
|
{{ t("changePassword.cancel") }}
|
||||||
|
</button>
|
||||||
|
<AsyncButton
|
||||||
|
type="submit"
|
||||||
|
mode="apply"
|
||||||
|
class="btn bg-error ml-10"
|
||||||
|
:disabled="!valid"
|
||||||
|
value="LOGIN"
|
||||||
|
@click="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-password-modal {
|
||||||
|
::v-deep .v--modal {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.card-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: start;
|
||||||
|
& > div {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-password {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
form {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -192,7 +192,7 @@ export default {
|
||||||
groups() {
|
groups() {
|
||||||
const map = {};
|
const map = {};
|
||||||
const defaultGroup = 'Questions';
|
const defaultGroup = 'Questions';
|
||||||
let weight = 1;
|
let weight = this.shownQuestions.length;
|
||||||
|
|
||||||
for ( const q of this.shownQuestions ) {
|
for ( const q of this.shownQuestions ) {
|
||||||
if ( q.group ) {
|
if ( q.group ) {
|
||||||
|
|
@ -202,7 +202,7 @@ export default {
|
||||||
map[normalized] = {
|
map[normalized] = {
|
||||||
name: q.group || defaultGroup,
|
name: q.group || defaultGroup,
|
||||||
questions: [],
|
questions: [],
|
||||||
weight: weight++,
|
weight: weight--,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const getters = {
|
||||||
bulkAction.enabled = state.tableSelected.length > 0 && actionEnabledForSomeSelected;
|
bulkAction.enabled = state.tableSelected.length > 0 && actionEnabledForSomeSelected;
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out.sort((a, b) => (b.weight || 0) - (a.weight || 0));
|
||||||
},
|
},
|
||||||
|
|
||||||
options(state = stateSchema) {
|
options(state = stateSchema) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
import Checkbox from '@/components/form/Checkbox';
|
||||||
|
import Password from '@/components/form/Password';
|
||||||
|
import { NORMAN } from '@/config/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox, Banner, Password
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async fetch() {
|
||||||
|
if (this.principal.provider === 'local' && !!this.principal.loginName) {
|
||||||
|
this.username = this.principal.loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await this.$store.dispatch('rancher/findAll', {
|
||||||
|
type: NORMAN.USER,
|
||||||
|
opt: { url: '/v3/users', filter: { me: true } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users && users.length === 1) {
|
||||||
|
this.username = users[0].username;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data(ctx) {
|
||||||
|
return {
|
||||||
|
username: '',
|
||||||
|
errorMessages: [],
|
||||||
|
pCanShowMissmatchedPassword: false,
|
||||||
|
pIsRandomGenerated: false,
|
||||||
|
form: {
|
||||||
|
deleteKeys: false,
|
||||||
|
currentP: '',
|
||||||
|
newP: '',
|
||||||
|
genP: '',
|
||||||
|
confirmP: ''
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
|
isRandomGenerated: {
|
||||||
|
get() {
|
||||||
|
return this.pIsRandomGenerated;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(isRandomGenerated) {
|
||||||
|
this.pIsRandomGenerated = isRandomGenerated;
|
||||||
|
this.errorMessages = [];
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
passwordGen: {
|
||||||
|
get() {
|
||||||
|
return this.form.genP;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(p) {
|
||||||
|
this.form.genP = p;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
passwordCurrent: {
|
||||||
|
get() {
|
||||||
|
return this.form.currentP;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(p) {
|
||||||
|
this.form.currentP = p;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
passwordNew: {
|
||||||
|
get() {
|
||||||
|
return this.form.newP;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(p) {
|
||||||
|
this.form.newP = p;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
passwordConfirm: {
|
||||||
|
get() {
|
||||||
|
return this.form.confirmP;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(p) {
|
||||||
|
this.form.confirmP = p;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
passwordConfirmBlurred: {
|
||||||
|
get() {
|
||||||
|
return this.pCanShowMissmatchedPassword;
|
||||||
|
},
|
||||||
|
|
||||||
|
set(p) {
|
||||||
|
this.pCanShowMissmatchedPassword = p;
|
||||||
|
this.validate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
password() {
|
||||||
|
return this.isRandomGenerated ? this.passwordGen : this.passwordNew;
|
||||||
|
},
|
||||||
|
|
||||||
|
principal() {
|
||||||
|
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
passwordsMatch() {
|
||||||
|
const match = this.passwordNew === this.passwordConfirm;
|
||||||
|
|
||||||
|
this.errorMessages = this.passwordConfirmBlurred && !match ? [this.t('changePassword.errors.missmatchedPassword')] : [];
|
||||||
|
|
||||||
|
return match;
|
||||||
|
},
|
||||||
|
validate() {
|
||||||
|
this.$emit('valid', this.isRandomGenerated ? !!this.passwordCurrent : this.passwordsMatch() && !!this.passwordCurrent && this.passwordNew);
|
||||||
|
this.$emit('input', this.password);
|
||||||
|
},
|
||||||
|
async submit() {
|
||||||
|
await this.changePassword();
|
||||||
|
if (this.form.deleteKeys) {
|
||||||
|
await this.deleteKeys();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async changePassword() {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('rancher/collectionAction', {
|
||||||
|
type: NORMAN.USER,
|
||||||
|
actionName: 'changepassword',
|
||||||
|
body: {
|
||||||
|
currentPassword: this.form.currentP,
|
||||||
|
newPassword: this.isRandomGenerated ? this.form.genP : this.form.newP
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.errorMessages = [err.message || this.t('changePassword.errors.failedToChange')];
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async deleteKeys() {
|
||||||
|
try {
|
||||||
|
const tokens = await this.$store.dispatch('rancher/findAll', {
|
||||||
|
type: NORMAN.TOKEN,
|
||||||
|
opt: {
|
||||||
|
// Ensure we have any new tokens since last fetched... and that we don't attempt to delete previously deleted tokens
|
||||||
|
force: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(tokens.reduce((res, token) => {
|
||||||
|
if (!token.current) {
|
||||||
|
res.push(token.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, []));
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message) {
|
||||||
|
this.errorMessages = [err.message];
|
||||||
|
} else if (err.length > 1) {
|
||||||
|
this.errorMessages = [this.t('changePassword.errors.failedDeleteKeys')];
|
||||||
|
} else {
|
||||||
|
this.errorMessages = [this.t('changePassword.errors.failedDeleteKey')];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="change-password">
|
||||||
|
<div class="form">
|
||||||
|
<div class="fields">
|
||||||
|
<Checkbox v-model="form.deleteKeys" :label="t('changePassword.keys')" class="mt-10" />
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
:value="username"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="password"
|
||||||
|
:value="password"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<Password
|
||||||
|
v-model="passwordCurrent"
|
||||||
|
class="mt-10"
|
||||||
|
:label="t('changePassword.currentPassword')"
|
||||||
|
></Password>
|
||||||
|
<Password
|
||||||
|
v-if="isRandomGenerated"
|
||||||
|
v-model="passwordGen"
|
||||||
|
class="mt-10"
|
||||||
|
:is-random="true"
|
||||||
|
:label="t('changePassword.randomGen.generated')"
|
||||||
|
/>
|
||||||
|
<div v-else class="userGen">
|
||||||
|
<Password
|
||||||
|
v-model="passwordNew"
|
||||||
|
class="mt-10"
|
||||||
|
:label="t('changePassword.userGen.newPassword')"
|
||||||
|
/>
|
||||||
|
<Password
|
||||||
|
v-model="passwordConfirm"
|
||||||
|
class="mt-10"
|
||||||
|
:label="t('changePassword.userGen.confirmPassword')"
|
||||||
|
@blur="passwordConfirmBlurred = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Checkbox v-model="isRandomGenerated" :label="t('changePassword.generatePassword')" class="mt-10 type" />
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMessages && errorMessages.length" class="text-error">
|
||||||
|
<Banner v-for="(err, i) in errorMessages" :key="i" color="error" :label="err" class="mb-0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.change-password {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.fields{
|
||||||
|
height: 215px;
|
||||||
|
#username, #password {
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
background-size: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -80,6 +80,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlur() {
|
onBlur() {
|
||||||
|
this.$emit('blur');
|
||||||
this.onBlurLabeled();
|
this.onBlurLabeled();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import { CHARSET, randomStr } from '@/utils/string';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { LabeledInput },
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
isRandom: {
|
||||||
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
default: '',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
autocomplete: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { reveal: false };
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
password: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
attributes() {
|
||||||
|
const attributes = { };
|
||||||
|
|
||||||
|
if (this.name) {
|
||||||
|
attributes.id = this.name;
|
||||||
|
attributes.name = this.name;
|
||||||
|
}
|
||||||
|
if (this.autocomplete) {
|
||||||
|
attributes.autocomplete = this.autocomplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.isRandom) {
|
||||||
|
this.generatePassword();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generatePassword() {
|
||||||
|
this.password = randomStr(16, CHARSET.ALPHA_NUM);
|
||||||
|
},
|
||||||
|
show(reveal) {
|
||||||
|
this.reveal = reveal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="password">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="password"
|
||||||
|
v-bind="attributes"
|
||||||
|
:type="isRandom || reveal ? 'text' : 'password'"
|
||||||
|
:readonly="isRandom"
|
||||||
|
:label="label"
|
||||||
|
:required="!isRandom"
|
||||||
|
:disabled="isRandom"
|
||||||
|
@blur="$emit('blur', $event)"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<div v-if="isRandom" class="addon">
|
||||||
|
<a href="#" @click.prevent.stop="$copyText(password)">{{ t('action.copy') }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-else class="addon">
|
||||||
|
<a v-if="reveal" href="#" @click.prevent.stop="reveal = false">{{ t('action.hide') }}</a>
|
||||||
|
<a v-else href="#" @click.prevent.stop="reveal=true">{{ t('action.show') }}</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</LabeledInput>
|
||||||
|
<div v-if="isRandom" class="mt-10 genPassword">
|
||||||
|
<a href="#" @click.prevent.stop="generatePassword"><i class="icon icon-refresh" /> {{ t('changePassword.newGeneratedPassword') }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.password {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.labeled-input {
|
||||||
|
.addon {
|
||||||
|
padding-left: 12px;
|
||||||
|
min-width: 65px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.genPassword {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -170,7 +170,7 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
&.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
.radio-container {
|
.radio-container {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export default {
|
||||||
|
|
||||||
addPrefix: {
|
addPrefix: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
suffix: {
|
suffix: {
|
||||||
|
|
|
||||||
|
|
@ -81,22 +81,22 @@ export default {
|
||||||
<ClusterSwitcher v-if="isMultiCluster && currentProduct && currentProduct.showClusterSwitcher" />
|
<ClusterSwitcher v-if="isMultiCluster && currentProduct && currentProduct.showClusterSwitcher" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user">
|
<div class="user" tabindex="0" @blur="showMenu(false)" @click="showMenu(true)" @focus.capture="showMenu(true)">
|
||||||
<v-popover
|
<v-popover
|
||||||
ref="popover"
|
ref="popover"
|
||||||
placement="bottom"
|
placement="bottom-end"
|
||||||
offset="-10"
|
offset="-10"
|
||||||
trigger="manual"
|
trigger="manual"
|
||||||
:delay="{show: 0, hide: 200}"
|
:delay="{show: 0, hide: 0}"
|
||||||
:popper-options="{modifiers: { flip: { enabled: false } } }"
|
:popper-options="{modifiers: { flip: { enabled: false } } }"
|
||||||
|
:container="false"
|
||||||
>
|
>
|
||||||
<div class="text-right" @mouseover="showMenu(true)" @click="showMenu(true)">
|
<div class="user-image text-right hand">
|
||||||
<img v-if="principal && principal.avatarSrc" :src="principal.avatarSrc" :class="{'avatar-round': principal.roundAvatar}" width="40" height="40" />
|
<img v-if="principal && principal.avatarSrc" :src="principal.avatarSrc" :class="{'avatar-round': principal.roundAvatar}" width="40" height="40" />
|
||||||
<i v-else class="icon icon-user icon-3x avatar" />
|
<i v-else class="icon icon-user icon-3x avatar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template slot="popover">
|
<template slot="popover">
|
||||||
<ul class="list-unstyled dropdown" @mouseleave="showMenu(false)">
|
<ul class="list-unstyled dropdown" @click.stop="showMenu(false)">
|
||||||
<li v-if="authEnabled" class="user-info">
|
<li v-if="authEnabled" class="user-info">
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
<i class="icon icon-lg icon-user" /> {{ principal.loginName }}
|
<i class="icon icon-lg icon-user" /> {{ principal.loginName }}
|
||||||
|
|
@ -105,15 +105,15 @@ export default {
|
||||||
{{ principal.name }}
|
{{ principal.name }}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<div @click="showMenu(false)">
|
<div>
|
||||||
<nuxt-link tag="li" :to="{name: 'account'}" class="hand">
|
|
||||||
<a>API Keys</a>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link tag="li" :to="{name: 'prefs'}" class="hand">
|
<nuxt-link tag="li" :to="{name: 'prefs'}" class="hand">
|
||||||
<a>Preferences <i class="icon icon-fw icon-gear" /></a>
|
<a>Preferences <i class="icon icon-fw icon-gear" /></a>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
<nuxt-link tag="li" :to="{name: 'account'}" class="hand">
|
||||||
|
<a>Account & API Keys <i class="icon icon-fw icon-user" /></a>
|
||||||
|
</nuxt-link>
|
||||||
<nuxt-link v-if="authEnabled" tag="li" :to="{name: 'auth-logout'}" class="pt-5 pb-5 hand">
|
<nuxt-link v-if="authEnabled" tag="li" :to="{name: 'auth-logout'}" class="pt-5 pb-5 hand">
|
||||||
<a>Log Out <i class="icon icon-fw icon-close" /></a>
|
<a @blur="showMenu(false)">Log Out <i class="icon icon-fw icon-close" /></a>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -236,6 +236,18 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .user {
|
> .user {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
.v-popover {
|
||||||
|
::v-deep .trigger {
|
||||||
|
.user-image > * {
|
||||||
|
@include form-focus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
grid-area: user;
|
grid-area: user;
|
||||||
background-color: var(--header-bg);
|
background-color: var(--header-bg);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ export const RBAC = { PRODUCT: 'management.cattle.io/ui-product' };
|
||||||
|
|
||||||
export const RKE = { EXTERNAL_IP: 'rke.cattle.io/external-ip' };
|
export const RKE = { EXTERNAL_IP: 'rke.cattle.io/external-ip' };
|
||||||
|
|
||||||
|
export const ISTIO = { AUTO_INJECTION: 'istio-injection' };
|
||||||
|
|
||||||
const CATTLE_REGEX = /cattle\.io\//;
|
const CATTLE_REGEX = /cattle\.io\//;
|
||||||
|
|
||||||
export const LABELS_TO_IGNORE_REGEX = [
|
export const LABELS_TO_IGNORE_REGEX = [
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,15 @@ export function init(store) {
|
||||||
});
|
});
|
||||||
|
|
||||||
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/github`, 'auth/github');
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/github`, 'auth/github');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/openldap`, 'auth/ldap/index');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/freeipa`, 'auth/ldap/index');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/activedirectory`, 'auth/ldap/index');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/ping`, 'auth/saml');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/shibboleth`, 'auth/saml');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/okta`, 'auth/saml');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/keycloak`, 'auth/saml');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/adfs`, 'auth/saml');
|
||||||
|
componentForType(`${ MANAGEMENT.AUTH_CONFIG }/googleoauth`, 'auth/googleoauth');
|
||||||
|
|
||||||
basicType([
|
basicType([
|
||||||
'config',
|
'config',
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,32 @@ export function init(store) {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
headers(BACKUP_RESTORE.BACKUP, [
|
headers(BACKUP_RESTORE.BACKUP, [
|
||||||
STATE,
|
{ ...STATE, value: 'Status' },
|
||||||
'Status',
|
|
||||||
NAME_HEADER,
|
NAME_HEADER,
|
||||||
'Location',
|
'Location',
|
||||||
'Type',
|
'Type',
|
||||||
'Latest-Backup',
|
{
|
||||||
AGE,
|
name: 'backupFilename',
|
||||||
|
labelKey: 'backupRestoreOperator.backupFilename',
|
||||||
|
value: 'status.filename'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nextBackup',
|
||||||
|
labelKey: 'backupRestoreOperator.nextBackup',
|
||||||
|
value: 'status.nextSnapshotAt',
|
||||||
|
formatter: 'Date'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nextBackup',
|
||||||
|
labelKey: 'backupRestoreOperator.lastBackup',
|
||||||
|
value: 'status.lastSnapshotTs',
|
||||||
|
formatter: 'Date'
|
||||||
|
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
headers(BACKUP_RESTORE.RESTORE, [
|
headers(BACKUP_RESTORE.RESTORE, [
|
||||||
STATE,
|
{ ...STATE, value: 'Status' },
|
||||||
'Status',
|
|
||||||
NAME_HEADER,
|
NAME_HEADER,
|
||||||
'Backup-Source',
|
'Backup-Source',
|
||||||
'Backup-File',
|
'Backup-File',
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export const STEVE = {
|
||||||
export const NORMAN = {
|
export const NORMAN = {
|
||||||
AUTH_CONFIG: 'authconfig',
|
AUTH_CONFIG: 'authconfig',
|
||||||
PRINCIPAL: 'principal',
|
PRINCIPAL: 'principal',
|
||||||
|
USER: 'user',
|
||||||
|
TOKEN: 'token',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Public (via Norman)
|
// Public (via Norman)
|
||||||
|
|
@ -126,6 +128,7 @@ export const MANAGEMENT = {
|
||||||
SETTING: 'management.cattle.io.setting',
|
SETTING: 'management.cattle.io.setting',
|
||||||
TOKEN: 'management.cattle.io.token',
|
TOKEN: 'management.cattle.io.token',
|
||||||
USER: 'management.cattle.io.user',
|
USER: 'management.cattle.io.user',
|
||||||
|
TOKEN: 'management.cattle.io.token',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CAPI = {
|
export const CAPI = {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import AsyncButton from '@/components/AsyncButton';
|
||||||
import CopyToClipboardText from '@/components/CopyToClipboardText.vue';
|
import CopyToClipboardText from '@/components/CopyToClipboardText.vue';
|
||||||
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
||||||
import { NORMAN, MANAGEMENT } from '@/config/types';
|
import { NORMAN, MANAGEMENT } from '@/config/types';
|
||||||
import { addObject, findBy } from '@/utils/array';
|
import { findBy } from '@/utils/array';
|
||||||
|
|
||||||
const NAME = 'github';
|
const NAME = 'github';
|
||||||
|
|
||||||
|
|
@ -105,6 +105,14 @@ export default {
|
||||||
AUTH_CONFIG() {
|
AUTH_CONFIG() {
|
||||||
return MANAGEMENT.AUTH_CONFIG;
|
return MANAGEMENT.AUTH_CONFIG;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toSave() {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
githubConfig: this.model,
|
||||||
|
description: 'Enable GitHub',
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -113,18 +121,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async reloadModel() {
|
|
||||||
this.originalModel = await this.$store.dispatch('rancher/find', {
|
|
||||||
type: NORMAN.AUTH_CONFIG,
|
|
||||||
id: NAME,
|
|
||||||
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ NAME }`, force: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
this.model = await this.$store.dispatch(`rancher/clone`, { resource: this.originalModel });
|
|
||||||
|
|
||||||
return this.model;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHost() {
|
updateHost() {
|
||||||
const match = this.targetUrl.match(/^(((https?):)?\/\/)?([^/]+)(\/.*)?$/);
|
const match = this.targetUrl.match(/^(((https?):)?\/\/)?([^/]+)(\/.*)?$/);
|
||||||
|
|
||||||
|
|
@ -138,67 +134,6 @@ export default {
|
||||||
this.model.hostname = match[4] || 'github.com';
|
this.model.hostname = match[4] || 'github.com';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(btnCb) {
|
|
||||||
this.errors = [];
|
|
||||||
|
|
||||||
const wasEnabled = this.model.enabled;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ( !wasEnabled ) {
|
|
||||||
const code = await this.$store.dispatch('auth/test', { provider: NAME, body: this.model });
|
|
||||||
|
|
||||||
this.model.enabled = true;
|
|
||||||
|
|
||||||
await this.model.doAction('testAndApply', {
|
|
||||||
code,
|
|
||||||
enabled: true,
|
|
||||||
githubConfig: this.model,
|
|
||||||
description: 'Enable GitHub',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload principals to get the new ones from GitHub
|
|
||||||
this.principals = await this.$store.dispatch('rancher/findAll', {
|
|
||||||
type: NORMAN.PRINCIPAL,
|
|
||||||
opt: { url: '/v3/principals', force: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
this.model.accessMode = 'restricted';
|
|
||||||
this.model.allowedPrincipalIds = this.model.allowedPrincipalIds || [];
|
|
||||||
|
|
||||||
if ( this.me && !this.model.allowedPrincipalIds.includes(this.me.id) ) {
|
|
||||||
addObject(this.model.allowedPrincipalIds, this.me.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.model.save();
|
|
||||||
|
|
||||||
await this.reloadModel();
|
|
||||||
|
|
||||||
btnCb(true);
|
|
||||||
|
|
||||||
if ( wasEnabled ) {
|
|
||||||
this.done();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = [err];
|
|
||||||
btnCb(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async disable(btnCb) {
|
|
||||||
try {
|
|
||||||
const clone = await this.$store.dispatch(`rancher/clone`, { resource: this.model });
|
|
||||||
|
|
||||||
clone.enabled = false;
|
|
||||||
await clone.save();
|
|
||||||
await this.reloadModel();
|
|
||||||
btnCb(true);
|
|
||||||
} catch (err) {
|
|
||||||
this.errors = [err];
|
|
||||||
btnCb(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -219,7 +154,7 @@ export default {
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
>
|
>
|
||||||
<template v-if="model.enabled">
|
<template v-if="model.enabled && !isSaving">
|
||||||
<Banner color="success clearfix">
|
<Banner color="success clearfix">
|
||||||
<div class="pull-left mt-10">
|
<div class="pull-left mt-10">
|
||||||
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
<script>
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
import Auth from '@/mixins/auth';
|
||||||
|
import CruResource from '@/components/CruResource';
|
||||||
|
import InfoBox from '@/components/InfoBox';
|
||||||
|
import Checkbox from '@/components/form/Checkbox';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
import AsyncButton from '@/components/AsyncButton';
|
||||||
|
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
||||||
|
import FileSelector from '@/components/form/FileSelector';
|
||||||
|
|
||||||
|
const NAME = 'googleoauth';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
CruResource,
|
||||||
|
InfoBox,
|
||||||
|
LabeledInput,
|
||||||
|
Banner,
|
||||||
|
Checkbox,
|
||||||
|
AllowedPrincipals,
|
||||||
|
AsyncButton,
|
||||||
|
FileSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, Auth],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
serverSetting: null,
|
||||||
|
errors: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
tArgs() {
|
||||||
|
let hostname = '';
|
||||||
|
|
||||||
|
if (process.client) {
|
||||||
|
hostname = window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
serverUrl: this.serverUrl,
|
||||||
|
provider: this.displayName,
|
||||||
|
username: this.principal.loginName || this.principal.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
NAME() {
|
||||||
|
return NAME;
|
||||||
|
},
|
||||||
|
|
||||||
|
toSave() {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
googleOauthConfig: this.model,
|
||||||
|
description: 'Enable Google OAuth',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="model"
|
||||||
|
:subtypes="[]"
|
||||||
|
:validation-passed="true"
|
||||||
|
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
|
||||||
|
:can-yaml="false"
|
||||||
|
:errors="errors"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<template v-if="model.enabled && !isSaving">
|
||||||
|
<Banner color="success clearfix">
|
||||||
|
<div class="pull-left mt-10">
|
||||||
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
|
</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div>{{ t(`authConfig.${NAME}.adminEmail`) }}: {{ model.adminEmail }}</div>
|
||||||
|
<div>{{ t(`authConfig.${NAME}.domain`) }}: {{ model.hostname }}</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<AllowedPrincipals provider="googleoauth" :auth-config="model" :mode="mode" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<Banner :label="t('authConfig.stateBanner.disabled', tArgs)" color="warning" />
|
||||||
|
<div :style="{'align-items':'center'}" class="row mb-20">
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.adminEmail"
|
||||||
|
:label="t(`authConfig.${NAME}.adminEmail`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.hostname"
|
||||||
|
:label="t(`authConfig.${NAME}.domain`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-2">
|
||||||
|
<Checkbox v-model="model.nestedGroupMembershipEnabled" :mode="mode" :label="t('authConfig.ldap.nestedGroupMembership.label')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<InfoBox class=" mt-20 mb-20 p-10">
|
||||||
|
<h3 v-html="t('authConfig.googleoauth.steps.1.title', tArgs, true)" />
|
||||||
|
<div v-html="t('authConfig.googleoauth.steps.1.body', tArgs, true)" />
|
||||||
|
</InfoBox>
|
||||||
|
<InfoBox class="mb-20 p-10">
|
||||||
|
<div class="row">
|
||||||
|
<h3 v-html="t('authConfig.googleoauth.steps.2.title', tArgs, true)" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6" v-html="t('authConfig.googleoauth.steps.2.body', tArgs, true)" />
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.oauthCredential"
|
||||||
|
:label="t(`authConfig.googleoauth.oauthCredentials.label`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
type="multiline"
|
||||||
|
:tooltip="t(`authConfig.googleoauth.oauthCredentials.tip`)"
|
||||||
|
:hover-tooltip="true"
|
||||||
|
/>
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'oauthCredential', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
<InfoBox class="mb-20 p-10">
|
||||||
|
<div class="row">
|
||||||
|
<h3 v-html="t('authConfig.googleoauth.steps.3.title', tArgs, true)" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6" v-html="t('authConfig.googleoauth.steps.3.body', tArgs, true)" />
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.serviceAccountCredential"
|
||||||
|
:label="t(`authConfig.googleoauth.serviceAccountCredentials.label`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
type="multiline"
|
||||||
|
:tooltip="t(`authConfig.googleoauth.serviceAccountCredentials.tip`)"
|
||||||
|
:hover-tooltip="true"
|
||||||
|
/>
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'serviceAccountCredential', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfoBox>
|
||||||
|
|
||||||
|
<div v-if="!model.enabled" class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info" v-html="t('authConfig.associatedWarning', tArgs, true)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CruResource>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
<script>
|
||||||
|
import RadioGroup from '@/components/form/RadioGroup';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import Checkbox from '@/components/form/Checkbox';
|
||||||
|
import UnitInput from '@/components/form/UnitInput';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
import FileSelector from '@/components/form/FileSelector';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RadioGroup,
|
||||||
|
LabeledInput,
|
||||||
|
Banner,
|
||||||
|
Checkbox,
|
||||||
|
UnitInput,
|
||||||
|
FileSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'edit'
|
||||||
|
},
|
||||||
|
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
if (!this.value.servers) {
|
||||||
|
this.value.servers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: this.value,
|
||||||
|
hostname: this.value.servers[0],
|
||||||
|
serverSetting: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
hostname(neu, old) {
|
||||||
|
this.value.servers[0] = neu;
|
||||||
|
},
|
||||||
|
'model.starttls'(neu) {
|
||||||
|
if (neu) {
|
||||||
|
this.model.tls = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'model.tls'(neu) {
|
||||||
|
if (neu) {
|
||||||
|
this.model.starttls = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div @input="$emit('input', model)">
|
||||||
|
<template>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="hostname" required :mode="mode" :label="t('authConfig.ldap.hostname')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-5">
|
||||||
|
<LabeledInput v-model="model.port" required :mode="mode" :label="t('authConfig.ldap.port')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col span-1">
|
||||||
|
<Checkbox v-model="model.tls" :mode="mode" class="full-height" :label="t('authConfig.ldap.tls')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="model.tls || model.starttls" class="row mb-20">
|
||||||
|
<div class="col span-12">
|
||||||
|
<LabeledInput v-model="model.certificate" required type="multiline" :mode="mode" :label="t('authConfig.ldap.cert')" />
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'certificate', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<UnitInput v-model="model.connectionTimeout" required :mode="mode" :label="t('authConfig.ldap.serverConnectionTimeout')" suffix="milliseconds" />
|
||||||
|
</div>
|
||||||
|
<div v-if="type==='openldap'" class="col span-6">
|
||||||
|
<Checkbox v-model="model.starttls" :tooltip="t('authConfig.ldap.starttls.tip')" :mode="mode" class="full-height" :label="t('authConfig.ldap.starttls.label')" />
|
||||||
|
</div>
|
||||||
|
<div v-if="type==='activedirectory'" class="col span-6">
|
||||||
|
<LabeledInput v-model="model.defaultLoginDomain" required :mode="mode" :label="t('authConfig.ldap.defaultLoginDomain')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner color="info" :label="t('authConfig.ldap.serviceAccountInfo')" />
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div v-if="type==='activedirectory'" class="col span-6">
|
||||||
|
<LabeledInput v-model="model.serviceAccountUsername" required :mode="mode" :label="t('authConfig.ldap.serviceAccountDN')" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="col span-6">
|
||||||
|
<LabeledInput v-model="model.serviceAccountDistinguishedName" required :mode="mode" :label="t('authConfig.ldap.serviceAccountDN')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.serviceAccountPassword" required type="password" :mode="mode" :label="t('authConfig.ldap.serviceAccountPassword')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userSearchBase" required :mode="mode" :label="t('authConfig.ldap.userSearchBase.label')" :placeholder="t('authConfig.ldap.userSearchBase.placeholder')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupSearchBase" type="password" :mode="mode" :placeholder="t('authConfig.ldap.groupSearchBase.placeholder')" :label="t('authConfig.ldap.groupSearchBase.label')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h3> {{ t('authConfig.ldap.customizeSchema') }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<h4>{{ t('authConfig.ldap.users') }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<h4>{{ t('authConfig.ldap.groups') }}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userObjectClass" :mode="mode" :label="t('authConfig.ldap.objectClass')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupObjectClass" :mode="mode" :label="t('authConfig.ldap.objectClass')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userNameAttribute" :mode="mode" :label="t('authConfig.ldap.usernameAttribute')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupNameAttribute" :mode="mode" :label="t('authConfig.ldap.nameAttribute')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userLoginAttribute" :mode="mode" :label="t('authConfig.ldap.loginAttribute')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupMemberUserAttribute" :mode="mode" :label="t('authConfig.ldap.groupMemberUserAttribute')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userMemberAttribute" :mode="mode" :label="t('authConfig.ldap.userMemberAttribute')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupSearchAttribute" :mode="mode" :label="t('authConfig.ldap.searchAttribute')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userSearchAttribute" :mode="mode" :label="t('authConfig.ldap.searchAttribute')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupSearchFilter" :mode="mode" :label="t('authConfig.ldap.searchFilter')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userSearchFilter" :mode="mode" :label="t('authConfig.ldap.searchFilter')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupMemberMappingAttribute" :mode="mode" :label="t('authConfig.ldap.groupMemberMappingAttribute')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.userEnabledAttribute" :mode="mode" :label="t('authConfig.ldap.userEnabledAttribute')" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.groupDNAttribute" :mode="mode" :label="t('authConfig.ldap.groupDNAttribute')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="model.disabledStatusBitmask" :mode="mode" :label="t('authConfig.ldap.disabledStatusBitmask')" />
|
||||||
|
</div>
|
||||||
|
<div class=" col span-6">
|
||||||
|
<RadioGroup
|
||||||
|
v-model="model.nestedGroupMembershipEnabled"
|
||||||
|
:mode="mode"
|
||||||
|
name="nested"
|
||||||
|
class="full-height"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('authConfig.ldap.nestedGroupMembership.options.nested'), t('authConfig.ldap.nestedGroupMembership.options.direct')]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script>
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
import Auth from '@/mixins/auth';
|
||||||
|
import CruResource from '@/components/CruResource';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
||||||
|
import AsyncButton from '@/components/AsyncButton';
|
||||||
|
import config from '@/edit/auth/ldap/config';
|
||||||
|
|
||||||
|
const AUTH_TYPE = 'ldap';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
Banner,
|
||||||
|
AllowedPrincipals,
|
||||||
|
AsyncButton,
|
||||||
|
config
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, Auth],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
serverSetting: null,
|
||||||
|
errors: null,
|
||||||
|
username: null,
|
||||||
|
password: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
tArgs() {
|
||||||
|
return {
|
||||||
|
provider: this.displayName,
|
||||||
|
username: this.principal.loginName || this.principal.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
AUTH_TYPE() {
|
||||||
|
return AUTH_TYPE;
|
||||||
|
},
|
||||||
|
|
||||||
|
toSave() {
|
||||||
|
let out = {
|
||||||
|
enabled: true,
|
||||||
|
ldapConfig: this.model,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.NAME === 'activedirectory') {
|
||||||
|
out = {
|
||||||
|
enabled: true,
|
||||||
|
activeDirectoryConfig: this.model,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="model"
|
||||||
|
:subtypes="[]"
|
||||||
|
:validation-passed="true"
|
||||||
|
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
|
||||||
|
:can-yaml="false"
|
||||||
|
:errors="errors"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<template v-if="model.enabled && !isSaving">
|
||||||
|
<Banner color="success clearfix">
|
||||||
|
<div class="pull-left mt-10">
|
||||||
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
|
</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
<div>Server: {{ serverUrl }}</div>
|
||||||
|
<div>Client ID: {{ model.serviceAccountDistinguishedName || model.serviceAccountUsername }}</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<AllowedPrincipals :provider="NAME" :auth-config="model" :mode="mode" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<h3>{{ t(`authConfig.ldap.${NAME}`) }}</h3>
|
||||||
|
<config v-model="model" :type="NAME" :mode="mode" />
|
||||||
|
|
||||||
|
<h4>{{ t('authConfig.testAndEnable') }}</h4>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="username"
|
||||||
|
:label="t(`authConfig.${AUTH_TYPE}.username`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
:label="t(`authConfig.${AUTH_TYPE}.password`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CruResource>
|
||||||
|
|
||||||
|
<div v-if="!model.enabled" class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info" v-html="t('authConfig.associatedWarning', tArgs, true)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
<script>
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
import Auth from '@/mixins/auth';
|
||||||
|
import CruResource from '@/components/CruResource';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import Checkbox from '@/components/form/Checkbox';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
import AllowedPrincipals from '@/components/auth/AllowedPrincipals';
|
||||||
|
import AsyncButton from '@/components/AsyncButton';
|
||||||
|
import FileSelector from '@/components/form/FileSelector';
|
||||||
|
import config from '@/edit/auth/ldap/config';
|
||||||
|
|
||||||
|
const AUTH_TYPE = 'ldap';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Loading,
|
||||||
|
CruResource,
|
||||||
|
LabeledInput,
|
||||||
|
Banner,
|
||||||
|
AllowedPrincipals,
|
||||||
|
Checkbox,
|
||||||
|
FileSelector,
|
||||||
|
AsyncButton,
|
||||||
|
config
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [CreateEditView, Auth],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
model: null,
|
||||||
|
errors: null,
|
||||||
|
serverSetting: null,
|
||||||
|
showLdap: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
baseUrl() {
|
||||||
|
return `${ this.model.tls ? 'https://' : 'http://' }${ this.model.hostname }`;
|
||||||
|
},
|
||||||
|
|
||||||
|
tArgs() {
|
||||||
|
return {
|
||||||
|
baseUrl: this.serverSetting,
|
||||||
|
provider: this.displayName,
|
||||||
|
username: this.principal.loginName || this.principal.name,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
AUTH_TYPE() {
|
||||||
|
return AUTH_TYPE;
|
||||||
|
},
|
||||||
|
|
||||||
|
toSave() {
|
||||||
|
return { enabled: true, ...this.model };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else>
|
||||||
|
<CruResource
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="model"
|
||||||
|
:subtypes="[]"
|
||||||
|
:validation-passed="true"
|
||||||
|
:finish-button-mode="model.enabled ? 'edit' : 'enable'"
|
||||||
|
:can-yaml="false"
|
||||||
|
:errors="errors"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<template v-if="model.enabled && !isSaving">
|
||||||
|
<Banner color="success clearfix">
|
||||||
|
<div class="pull-left mt-10">
|
||||||
|
{{ t('authConfig.stateBanner.enabled', tArgs) }}
|
||||||
|
</div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<AsyncButton mode="disable" size="sm" action-color="bg-error" @click="disable" />
|
||||||
|
</div>
|
||||||
|
</Banner>
|
||||||
|
|
||||||
|
<div>Server: {{ baseUrl }}</div>
|
||||||
|
<div>Display Name: {{ model.displayNameField }}</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<AllowedPrincipals provider="github" :auth-config="model" :mode="mode" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<h3>{{ t(`authConfig.saml.${NAME}`) }}</h3>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.displayNameField"
|
||||||
|
:label="t(`authConfig.saml.displayName`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.userNameField"
|
||||||
|
:label="t(`authConfig.saml.userName`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.uidField"
|
||||||
|
:label="t(`authConfig.saml.UID`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.groupsField"
|
||||||
|
:label="t(`authConfig.saml.groups`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.rancherApiHost"
|
||||||
|
:label="t(`authConfig.saml.api`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.spKey"
|
||||||
|
:label="t(`authConfig.saml.key`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
type="multiline"
|
||||||
|
/>
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'spKey', $event)" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.spCert"
|
||||||
|
:label="t(`authConfig.saml.cert`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
type="multiline"
|
||||||
|
/>
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'spCert', $event)" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-4">
|
||||||
|
<LabeledInput
|
||||||
|
v-model="model.idpMetadataContent"
|
||||||
|
:label="t(`authConfig.saml.metadata`)"
|
||||||
|
:mode="mode"
|
||||||
|
required
|
||||||
|
type="multiline"
|
||||||
|
/>
|
||||||
|
<FileSelector class="role-tertiary add mt-5" :label="t('generic.readFromFile')" :mode="mode" @selected="$set(model, 'idpMetadataContent', $event)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="NAME === 'shibboleth'">
|
||||||
|
<div class="row">
|
||||||
|
<Checkbox v-model="showLdap" :mode="mode" :label="t('authConfig.saml.showLdap')" />
|
||||||
|
</div>
|
||||||
|
<div class="row mt-20 mb-20">
|
||||||
|
<config v-if="showLdap" v-model="model.openLdapConfig" :type="NAME" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CruResource>
|
||||||
|
<div v-if="!model.enabled" class="row">
|
||||||
|
<div class="col span-12">
|
||||||
|
<Banner color="info" v-html="t('authConfig.associatedWarning', tArgs, true)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -173,26 +173,26 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.responder {
|
.send-to {
|
||||||
&, .target {
|
margin-left: -35px;
|
||||||
width: 100%;
|
}
|
||||||
}
|
.responder {
|
||||||
.send-to {
|
&, .target {
|
||||||
margin-left: -35px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unlabeled-select ::v-deep {
|
.unlabeled-select ::v-deep {
|
||||||
|
height: $input-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target ::v-deep {
|
||||||
|
& .input-container {
|
||||||
height: $input-height;
|
height: $input-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.target ::v-deep {
|
& .unlabeled-select {
|
||||||
& .input-container {
|
min-width: 35%;
|
||||||
height: $input-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .unlabeled-select {
|
|
||||||
min-width: 35%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -171,9 +171,8 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<NameNsDescription :mode="mode" :value="value" :namespaced="false" @change="name=value.metadata.name" />
|
<NameNsDescription :mode="mode" :value="value" :namespaced="false" @change="name=value.metadata.name" />
|
||||||
<template v-if="!!resourceSet">
|
<template v-if="!!resourceSet">
|
||||||
<div v-if="!isView || setSchedule" class="bordered-section">
|
<div class="bordered-section">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-if="!isView"
|
|
||||||
v-model="setSchedule"
|
v-model="setSchedule"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:label="t('backupRestoreOperator.schedule.label')"
|
:label="t('backupRestoreOperator.schedule.label')"
|
||||||
|
|
@ -183,7 +182,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
<div v-if="setSchedule" class="row mt-10 mb-10">
|
<div v-if="setSchedule" class="row mt-10 mb-10">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledInput v-model="value.spec.schedule" :mode="mode" :label="t('backupRestoreOperator.schedule.label')" :placeholder="t('backupRestoreOperator.schedule.placeholder')" />
|
<LabeledInput v-model="value.spec.schedule" type="cron" :mode="mode" :label="t('backupRestoreOperator.schedule.label')" :placeholder="t('backupRestoreOperator.schedule.placeholder')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<UnitInput v-model="value.spec.retentionCount" :suffix="t('backupRestoreOperator.retentionCount.units', {count: value.spec.retentionCount || 0})" :mode="mode" :label="t('backupRestoreOperator.retentionCount.label')" />
|
<UnitInput v-model="value.spec.retentionCount" :suffix="t('backupRestoreOperator.retentionCount.units', {count: value.spec.retentionCount || 0})" :mode="mode" :label="t('backupRestoreOperator.retentionCount.label')" />
|
||||||
|
|
@ -191,11 +190,10 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isView || useEncryption" class="bordered-section">
|
<div class="bordered-section">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
v-if="!isView"
|
|
||||||
v-model="useEncryption"
|
v-model="useEncryption"
|
||||||
name="useEncryption"
|
name="useEncryption"
|
||||||
:label="t('backupRestoreOperator.encryption')"
|
:label="t('backupRestoreOperator.encryption')"
|
||||||
|
|
|
||||||
132
edit/service.vue
132
edit/service.vue
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
import ArrayList from '@/components/form/ArrayList';
|
import ArrayList from '@/components/form/ArrayList';
|
||||||
import CreateEditView from '@/mixins/create-edit-view';
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
import KeyValue from '@/components/form/KeyValue';
|
import KeyValue from '@/components/form/KeyValue';
|
||||||
|
|
@ -16,15 +17,17 @@ import CruResource from '@/components/CruResource';
|
||||||
import Banner from '@/components/Banner';
|
import Banner from '@/components/Banner';
|
||||||
import Labels from '@/components/form/Labels';
|
import Labels from '@/components/form/Labels';
|
||||||
import { clone } from '@/utils/object';
|
import { clone } from '@/utils/object';
|
||||||
|
import { POD } from '@/config/types';
|
||||||
|
import { matching } from '@/utils/selector';
|
||||||
|
|
||||||
const SESSION_AFFINITY_ACTION_VALUES = {
|
const SESSION_AFFINITY_ACTION_VALUES = {
|
||||||
NONE: 'None',
|
NONE: 'None',
|
||||||
CLIENTIP: 'ClientIP'
|
CLIENTIP: 'ClientIP',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SESSION_AFFINITY_ACTION_LABELS = {
|
const SESSION_AFFINITY_ACTION_LABELS = {
|
||||||
NONE: 'servicesPage.affinity.actionLabels.none',
|
NONE: 'servicesPage.affinity.actionLabels.none',
|
||||||
CLIENTIP: 'servicesPage.affinity.actionLabels.clientIp'
|
CLIENTIP: 'servicesPage.affinity.actionLabels.clientIp',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SESSION_STICKY_TIME_DEFAULT = 10800;
|
const SESSION_STICKY_TIME_DEFAULT = 10800;
|
||||||
|
|
@ -45,22 +48,36 @@ export default {
|
||||||
ServicePorts,
|
ServicePorts,
|
||||||
Tab,
|
Tab,
|
||||||
Tabbed,
|
Tabbed,
|
||||||
UnitInput
|
UnitInput,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [CreateEditView],
|
mixins: [CreateEditView],
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
return this.loadPods();
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
if (!this?.value?.spec?.type) {
|
if (!this?.value?.spec?.type) {
|
||||||
if (!this.value?.spec) {
|
if (!this.value?.spec) {
|
||||||
this.$set(this.value, 'spec', {
|
this.$set(this.value, 'spec', {
|
||||||
ports: [],
|
ports: [],
|
||||||
sessionAffinity: 'None'
|
sessionAffinity: 'None',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matchingPods = {
|
||||||
|
matched: 0,
|
||||||
|
matches: [],
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
matchingPods,
|
||||||
|
allPods: [],
|
||||||
defaultServiceTypes: DEFAULT_SERVICE_TYPES,
|
defaultServiceTypes: DEFAULT_SERVICE_TYPES,
|
||||||
saving: false,
|
saving: false,
|
||||||
sessionAffinityActionLabels: Object.values(SESSION_AFFINITY_ACTION_LABELS)
|
sessionAffinityActionLabels: Object.values(SESSION_AFFINITY_ACTION_LABELS)
|
||||||
|
|
@ -68,7 +85,7 @@ export default {
|
||||||
.map(ucFirst),
|
.map(ucFirst),
|
||||||
sessionAffinityActionOptions: Object.values(
|
sessionAffinityActionOptions: Object.values(
|
||||||
SESSION_AFFINITY_ACTION_VALUES
|
SESSION_AFFINITY_ACTION_VALUES
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -112,18 +129,26 @@ export default {
|
||||||
|
|
||||||
this.$set(this.value.spec, 'type', serviceType);
|
this.$set(this.value.spec, 'type', serviceType);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
showAffinityTimeout() {
|
showAffinityTimeout() {
|
||||||
return this.value.spec.sessionAffinity === 'ClientIP' && !isEmpty(this.value.spec.sessionAffinityConfig);
|
return (
|
||||||
|
this.value.spec.sessionAffinity === 'ClientIP' &&
|
||||||
|
!isEmpty(this.value.spec.sessionAffinityConfig)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
hasClusterIp() {
|
hasClusterIp() {
|
||||||
return this.checkTypeIs('ClusterIP') || this.checkTypeIs('LoadBalancer') || this.checkTypeIs('NodePort');
|
return (
|
||||||
}
|
this.checkTypeIs('ClusterIP') ||
|
||||||
|
this.checkTypeIs('LoadBalancer') ||
|
||||||
|
this.checkTypeIs('NodePort')
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
'value.spec.selector': 'updateMatchingPods',
|
||||||
'value.spec.sessionAffinity'(val) {
|
'value.spec.sessionAffinity'(val) {
|
||||||
if (val === 'ClientIP') {
|
if (val === 'ClientIP') {
|
||||||
this.value.spec.sessionAffinityConfig = { clientIP: { timeoutSeconds: null } };
|
this.value.spec.sessionAffinityConfig = { clientIP: { timeoutSeconds: null } };
|
||||||
|
|
@ -139,7 +164,7 @@ export default {
|
||||||
) {
|
) {
|
||||||
delete this.value.spec.sessionAffinityConfig.clientIP.timeoutSeconds;
|
delete this.value.spec.sessionAffinityConfig.clientIP.timeoutSeconds;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
|
@ -153,6 +178,37 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
updateMatchingPods: throttle(function() {
|
||||||
|
const { allPods, value: { spec: { selector = { } } } } = this;
|
||||||
|
|
||||||
|
if (isEmpty(selector)) {
|
||||||
|
this.matchingPods = {
|
||||||
|
matched: 0,
|
||||||
|
total: allPods.length,
|
||||||
|
none: true,
|
||||||
|
sample: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const match = matching(allPods, selector);
|
||||||
|
|
||||||
|
this.matchingPods = {
|
||||||
|
matched: match.length,
|
||||||
|
total: allPods.length,
|
||||||
|
none: match.length === 0,
|
||||||
|
sample: match[0] ? match[0].nameDisplay : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, 250, { leading: true }),
|
||||||
|
|
||||||
|
async loadPods() {
|
||||||
|
try {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
this.allPods = await this.$store.dispatch(`${ inStore }/findAll`, { type: POD });
|
||||||
|
this.matchingPods.total = this.allPods.length;
|
||||||
|
} catch (e) { }
|
||||||
|
},
|
||||||
|
|
||||||
checkTypeIs(typeIn) {
|
checkTypeIs(typeIn) {
|
||||||
const { serviceType } = this;
|
const { serviceType } = this;
|
||||||
|
|
||||||
|
|
@ -187,8 +243,7 @@ export default {
|
||||||
this.value.spec.ports = this.targetPortsStrOrInt(this.value.spec.ports);
|
this.value.spec.ports = this.targetPortsStrOrInt(this.value.spec.ports);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -201,10 +256,10 @@ export default {
|
||||||
:subtypes="defaultServiceTypes"
|
:subtypes="defaultServiceTypes"
|
||||||
:validation-passed="true"
|
:validation-passed="true"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@error="e=>errors = e"
|
@error="(e) => (errors = e)"
|
||||||
@finish="save"
|
@finish="save"
|
||||||
@cancel="done"
|
@cancel="done"
|
||||||
@select-type="(st) => serviceType = st"
|
@select-type="(st) => (serviceType = st)"
|
||||||
@apply-hooks="() => applyHooks('_beforeSaveHooks')"
|
@apply-hooks="() => applyHooks('_beforeSaveHooks')"
|
||||||
>
|
>
|
||||||
<NameNsDescription v-if="!isView" :value="value" :mode="mode" />
|
<NameNsDescription v-if="!isView" :value="value" :mode="mode" />
|
||||||
|
|
@ -226,12 +281,18 @@ export default {
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:label="t('servicesPage.externalName.input.label')"
|
:label="t('servicesPage.externalName.input.label')"
|
||||||
:placeholder="t('servicesPage.externalName.placeholder')"
|
:placeholder="t('servicesPage.externalName.placeholder')"
|
||||||
|
:required="true"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab v-else name="define-service-ports" :label="t('servicesPage.ips.define')" :weight="10">
|
<Tab
|
||||||
|
v-else
|
||||||
|
name="define-service-ports"
|
||||||
|
:label="t('servicesPage.ips.define')"
|
||||||
|
:weight="10"
|
||||||
|
>
|
||||||
<ServicePorts
|
<ServicePorts
|
||||||
v-model="value.spec.ports"
|
v-model="value.spec.ports"
|
||||||
class="col span-12"
|
class="col span-12"
|
||||||
|
|
@ -247,7 +308,9 @@ export default {
|
||||||
>
|
>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col span-12">
|
<div class="col span-12">
|
||||||
<Banner v-if="showSelectorWarning" color="warning" :label="t('servicesPage.selectors.helpText')" />
|
<Banner :color="(matchingPods.none ? 'warning' : 'success')">
|
||||||
|
<span v-html="t('servicesPage.selectors.matchingPods.matchesSome', matchingPods)" />
|
||||||
|
</Banner>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -258,24 +321,27 @@ export default {
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:initial-empty-row="true"
|
:initial-empty-row="true"
|
||||||
:protip="false"
|
:protip="false"
|
||||||
@input="e=>$set(value.spec, 'selector', e)"
|
@input="(e) => $set(value.spec, 'selector', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab name="ips" :label="t('servicesPage.ips.label')" :tooltip="t('servicesPage.ips.external.protip')">
|
<Tab
|
||||||
<div
|
name="ips"
|
||||||
v-if="hasClusterIp"
|
:label="t('servicesPage.ips.label')"
|
||||||
class="row mb-20"
|
:tooltip="t('servicesPage.ips.external.protip')"
|
||||||
>
|
>
|
||||||
|
<div v-if="hasClusterIp" class="row mb-20">
|
||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<LabeledInput
|
<LabeledInput
|
||||||
v-model="value.spec.clusterIP"
|
v-model="value.spec.clusterIP"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:label="t('servicesPage.ips.input.label')"
|
:label="t('servicesPage.ips.input.label')"
|
||||||
:placeholder="t('servicesPage.ips.input.placeholder')"
|
:placeholder="t('servicesPage.ips.input.placeholder')"
|
||||||
:tooltip-key="hasClusterIp ? 'servicesPage.ips.clusterIpHelpText' : null"
|
:tooltip-key="
|
||||||
@input="e=>$set(value.spec, 'clusterIP', e)"
|
hasClusterIp ? 'servicesPage.ips.clusterIpHelpText' : null
|
||||||
|
"
|
||||||
|
@input="(e) => $set(value.spec, 'clusterIP', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -288,7 +354,7 @@ export default {
|
||||||
:value-placeholder="t('servicesPage.ips.external.placeholder')"
|
:value-placeholder="t('servicesPage.ips.external.placeholder')"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:protip="false"
|
:protip="false"
|
||||||
@input="e=>$set(value.spec, 'externalIPs', e)"
|
@input="(e) => $set(value.spec, 'externalIPs', e)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -313,10 +379,22 @@ export default {
|
||||||
<div v-if="showAffinityTimeout" class="col span-6">
|
<div v-if="showAffinityTimeout" class="col span-6">
|
||||||
<UnitInput
|
<UnitInput
|
||||||
v-model="value.spec.sessionAffinityConfig.clientIP.timeoutSeconds"
|
v-model="value.spec.sessionAffinityConfig.clientIP.timeoutSeconds"
|
||||||
:suffix="t('suffix.seconds', {count: value.spec.sessionAffinityConfig.clientIP.timeoutSeconds})"
|
:suffix="
|
||||||
|
t('suffix.seconds', {
|
||||||
|
count:
|
||||||
|
value.spec.sessionAffinityConfig.clientIP.timeoutSeconds,
|
||||||
|
})
|
||||||
|
"
|
||||||
:label="t('servicesPage.affinity.timeout.label')"
|
:label="t('servicesPage.affinity.timeout.label')"
|
||||||
:placeholder="t('servicesPage.affinity.timeout.placeholder')"
|
:placeholder="t('servicesPage.affinity.timeout.placeholder')"
|
||||||
@input="e=>$set(value.spec.sessionAffinityConfig.clientIP, 'timeoutSeconds', e)"
|
@input="
|
||||||
|
(e) =>
|
||||||
|
$set(
|
||||||
|
value.spec.sessionAffinityConfig.clientIP,
|
||||||
|
'timeoutSeconds',
|
||||||
|
e
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { NORMAN, MANAGEMENT } from '@/config/types';
|
||||||
|
import { addObject, findBy } from '@/utils/array';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch() {
|
||||||
|
const NAME = this.$route.params.id;
|
||||||
|
const originalModel = await this.$store.dispatch('rancher/find', {
|
||||||
|
type: NORMAN.AUTH_CONFIG,
|
||||||
|
id: NAME,
|
||||||
|
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ NAME }`, force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverUrl = await this.$store.dispatch('management/find', {
|
||||||
|
type: MANAGEMENT.SETTING,
|
||||||
|
id: 'server-url',
|
||||||
|
opt: { url: `/v1/{ MANAGEMENT.SETTING }/server-url` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if ( serverUrl ) {
|
||||||
|
this.serverSetting = serverUrl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model = await this.$store.dispatch(`rancher/clone`, { resource: originalModel });
|
||||||
|
if (NAME === 'shibboleth' && !this.model.openLdapConfig) {
|
||||||
|
this.model.openLdapConfig = {};
|
||||||
|
this.showLdap = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { isSaving: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
me() {
|
||||||
|
const out = findBy(this.principals, 'me', true);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
serverUrl() {
|
||||||
|
if ( this.serverSetting ) {
|
||||||
|
return this.serverSetting;
|
||||||
|
} else if ( process.client ) {
|
||||||
|
return window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
principal() {
|
||||||
|
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
|
||||||
|
},
|
||||||
|
|
||||||
|
displayName() {
|
||||||
|
return this.t(`model.authConfig.provider.${ this.NAME }`);
|
||||||
|
},
|
||||||
|
|
||||||
|
NAME() {
|
||||||
|
return this.$route.params.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
AUTH_CONFIG() {
|
||||||
|
return MANAGEMENT.AUTH_CONFIG;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async save(btnCb) {
|
||||||
|
const configType = this.value.configType;
|
||||||
|
|
||||||
|
this.isSaving = true;
|
||||||
|
this.errors = [];
|
||||||
|
const wasEnabled = this.model.enabled;
|
||||||
|
let obj = this.toSave;
|
||||||
|
|
||||||
|
if (!obj) {
|
||||||
|
obj = this.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ( !wasEnabled ) {
|
||||||
|
if (configType === 'oauth') {
|
||||||
|
const code = await this.$store.dispatch('auth/test', { provider: this.model.id, body: this.model });
|
||||||
|
|
||||||
|
this.model.enabled = true;
|
||||||
|
obj.code = code;
|
||||||
|
}
|
||||||
|
if (configType === 'saml') {
|
||||||
|
this.model.enabled = true;
|
||||||
|
if (!this.model.accessMode) {
|
||||||
|
this.model.accessMode = 'unrestricted';
|
||||||
|
}
|
||||||
|
await this.model.save();
|
||||||
|
await this.model.doAction('testAndEnable', obj);
|
||||||
|
} else {
|
||||||
|
this.model.enabled = true;
|
||||||
|
if (!this.model.accessMode) {
|
||||||
|
this.model.accessMode = 'unrestricted';
|
||||||
|
}
|
||||||
|
await this.model.doAction('testAndApply', obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload principals to get the new ones from the provider
|
||||||
|
this.principals = await this.$store.dispatch('rancher/findAll', {
|
||||||
|
type: NORMAN.PRINCIPAL,
|
||||||
|
opt: { url: '/v3/principals', force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.model.allowedPrincipalIds = this.model.allowedPrincipalIds || [];
|
||||||
|
if ( this.me && !this.model.allowedPrincipalIds.includes(this.me.id) ) {
|
||||||
|
addObject(this.model.allowedPrincipalIds, this.me.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configType === 'oauth') {
|
||||||
|
await this.model.save();
|
||||||
|
await this.reloadModel();
|
||||||
|
}
|
||||||
|
this.isSaving = false;
|
||||||
|
btnCb(true);
|
||||||
|
if ( wasEnabled ) {
|
||||||
|
this.done();
|
||||||
|
}
|
||||||
|
// this.$router.applyQuery( { mode: 'view' } );
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err];
|
||||||
|
btnCb(false);
|
||||||
|
this.model.enabled = wasEnabled;
|
||||||
|
this.isSaving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async disable(btnCb) {
|
||||||
|
try {
|
||||||
|
if (this.model.hasAction('disable')) {
|
||||||
|
await this.model.doAction('disable');
|
||||||
|
} else {
|
||||||
|
const clone = await this.$store.dispatch(`rancher/clone`, { resource: this.model });
|
||||||
|
|
||||||
|
clone.enabled = false;
|
||||||
|
await clone.save();
|
||||||
|
}
|
||||||
|
await this.reloadModel();
|
||||||
|
|
||||||
|
btnCb(true);
|
||||||
|
} catch (err) {
|
||||||
|
this.errors = [err];
|
||||||
|
btnCb(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadModel() {
|
||||||
|
this.originalModel = await this.$store.dispatch('rancher/find', {
|
||||||
|
type: NORMAN.AUTH_CONFIG,
|
||||||
|
id: this.NAME,
|
||||||
|
opt: { url: `/v3/${ NORMAN.AUTH_CONFIG }/${ this.NAME }`, force: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.model = await this.$store.dispatch(`rancher/clone`, { resource: this.originalModel });
|
||||||
|
|
||||||
|
return this.model;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { insertAt } from '@/utils/array';
|
import { insertAt } from '@/utils/array';
|
||||||
|
import { set } from '@/utils/object';
|
||||||
|
|
||||||
const configType = {
|
const configType = {
|
||||||
activedirectory: 'ldap',
|
activedirectory: 'ldap',
|
||||||
|
|
@ -61,5 +62,27 @@ export default {
|
||||||
await this.save();
|
await this.save();
|
||||||
this.currentRouter().push({ name: 'c-cluster-auth-config' });
|
this.currentRouter().push({ name: 'c-cluster-auth-config' });
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
applyDefaults() {
|
||||||
|
return () => {
|
||||||
|
switch (this.configType) {
|
||||||
|
case 'saml':
|
||||||
|
set(this, 'accessMode', 'unrestricted');
|
||||||
|
|
||||||
|
if (this.id === 'shibboleth' && !this.openLdapConfig) {
|
||||||
|
set(this, 'openLdapConfig', {});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ldap':
|
||||||
|
set(this, 'servers', []);
|
||||||
|
set(this, 'accessMode', 'unrestricted');
|
||||||
|
set(this, 'starttls', false);
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,41 @@
|
||||||
import SYSTEM_NAMESPACES from '@/config/system-namespaces';
|
import SYSTEM_NAMESPACES from '@/config/system-namespaces';
|
||||||
import { PROJECT, SYSTEM_NAMESPACE } from '@/config/labels-annotations';
|
import { PROJECT, SYSTEM_NAMESPACE, ISTIO as ISTIO_LABELS } from '@/config/labels-annotations';
|
||||||
import { MANAGEMENT } from '@/config/types';
|
import { ISTIO, MANAGEMENT } from '@/config/types';
|
||||||
|
|
||||||
import { escapeHtml } from '@/utils/string';
|
import { escapeHtml } from '@/utils/string';
|
||||||
|
import { insertAt, isArray } from '@/utils/array';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
|
_availableActions() {
|
||||||
|
const out = this._standardActions;
|
||||||
|
|
||||||
|
insertAt(out, 0, { divider: true });
|
||||||
|
if (this.istioInstalled) {
|
||||||
|
insertAt(out, 0, {
|
||||||
|
action: 'enableAutoInjection',
|
||||||
|
label: this.t('namespace.enableAutoInjection'),
|
||||||
|
bulkable: true,
|
||||||
|
bulkAction: 'enableAutoInjection',
|
||||||
|
enabled: !this.injectionEnabled,
|
||||||
|
icon: 'icon icon-plus',
|
||||||
|
weight: 2
|
||||||
|
|
||||||
|
});
|
||||||
|
insertAt(out, 0, {
|
||||||
|
action: 'disableAutoInjection',
|
||||||
|
label: this.t('namespace.disableAutoInjection'),
|
||||||
|
bulkable: true,
|
||||||
|
bulkAction: 'disableAutoInjection',
|
||||||
|
enabled: this.injectionEnabled,
|
||||||
|
icon: 'icon icon-minus',
|
||||||
|
weight: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
isSystem() {
|
isSystem() {
|
||||||
if ( this.metadata?.annotations?.[SYSTEM_NAMESPACE] === 'true' ) {
|
if ( this.metadata?.annotations?.[SYSTEM_NAMESPACE] === 'true' ) {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -51,5 +83,40 @@ export default {
|
||||||
|
|
||||||
projectNameSort() {
|
projectNameSort() {
|
||||||
return this.project?.nameSort || '';
|
return this.project?.nameSort || '';
|
||||||
}
|
},
|
||||||
|
|
||||||
|
istioInstalled() {
|
||||||
|
const schema = this.$rootGetters['cluster/schemaFor'](ISTIO.GATEWAY);
|
||||||
|
|
||||||
|
return !!schema;
|
||||||
|
},
|
||||||
|
|
||||||
|
injectionEnabled() {
|
||||||
|
return this.labels[ISTIO_LABELS.AUTO_INJECTION] === 'enabled';
|
||||||
|
},
|
||||||
|
|
||||||
|
enableAutoInjection() {
|
||||||
|
return (namespaces = this, enable = true) => {
|
||||||
|
if (!isArray(namespaces)) {
|
||||||
|
namespaces = [namespaces];
|
||||||
|
}
|
||||||
|
namespaces.forEach((ns) => {
|
||||||
|
if (!enable && ns?.metadata?.labels) {
|
||||||
|
delete ns.metadata.labels[ISTIO_LABELS.AUTO_INJECTION];
|
||||||
|
} else {
|
||||||
|
if (!ns.metadata.labels) {
|
||||||
|
ns.metadata.labels = {};
|
||||||
|
}
|
||||||
|
ns.metadata.labels[ISTIO_LABELS.AUTO_INJECTION] = 'enabled';
|
||||||
|
}
|
||||||
|
ns.save();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
disableAutoInjection() {
|
||||||
|
return (namespaces = this) => {
|
||||||
|
this.enableAutoInjection(namespaces, false);
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { colorForState, stateDisplay } from '@/plugins/steve/resource-instance';
|
||||||
|
import { findBy } from '@/utils/array';
|
||||||
|
import { get } from '@/utils/object';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
readyMessage() {
|
||||||
|
const conditions = get(this, 'status.conditions');
|
||||||
|
const readyMessage = (findBy(conditions, 'type', 'Ready') || {}).message ;
|
||||||
|
|
||||||
|
return readyMessage;
|
||||||
|
},
|
||||||
|
colorForState() {
|
||||||
|
if (this.readyMessage) {
|
||||||
|
return colorForState(this.readyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorForState();
|
||||||
|
},
|
||||||
|
|
||||||
|
stateDisplay() {
|
||||||
|
if (this.readyMessage) {
|
||||||
|
return stateDisplay(this.readyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
|
import { colorForState, stateDisplay } from '@/plugins/steve/resource-instance';
|
||||||
|
import { findBy } from '@/utils/array';
|
||||||
|
import { get } from '@/utils/object';
|
||||||
export default {
|
export default {
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this?.metadata?.state?.error;
|
return this?.metadata?.state?.error;
|
||||||
},
|
},
|
||||||
|
readyMessage() {
|
||||||
|
const conditions = get(this, 'status.conditions');
|
||||||
|
const readyMessage = (findBy(conditions, 'type', 'Ready') || {}).message ;
|
||||||
|
|
||||||
|
return readyMessage;
|
||||||
|
},
|
||||||
|
colorForState() {
|
||||||
|
if (this.readyMessage) {
|
||||||
|
return colorForState(this.readyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorForState();
|
||||||
|
},
|
||||||
|
|
||||||
|
stateDisplay() {
|
||||||
|
if (this.readyMessage) {
|
||||||
|
return stateDisplay(this.readyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,67 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import PromptChangePassword from '@/components/PromptChangePassword';
|
||||||
import { MANAGEMENT } from '@/config/types';
|
import { NORMAN, MANAGEMENT } from '@/config/types';
|
||||||
import Loading from '@/components/Loading';
|
import Loading from '@/components/Loading';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import ResourceTable from '@/components/ResourceTable';
|
import ResourceTable from '@/components/ResourceTable';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Loading,
|
PromptChangePassword, Loading, ResourceTable
|
||||||
ResourceTable,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
this.rows = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.TOKEN, opt: { force: true } });
|
this.canChangePassword = await this.calcCanChangePassword();
|
||||||
|
this.apiKeys = await this.fetchApiKeys();
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return { rows: null };
|
return {
|
||||||
|
apiKeys: null,
|
||||||
|
canChangePassword: false
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
computed: {
|
|
||||||
...mapGetters({ t: 'i18n/t' }),
|
...mapGetters({ t: 'i18n/t' }),
|
||||||
|
|
||||||
headers() {
|
apiKeyheaders() {
|
||||||
return this.$store.getters['type-map/headersFor'](this.schema);
|
return this.$store.getters['type-map/headersFor'](this.apiKeySchema);
|
||||||
},
|
},
|
||||||
|
|
||||||
schema() {
|
apiKeySchema() {
|
||||||
const inStore = this.$store.getters['currentProduct'].inStore;
|
return this.$store.getters[`management/schemaFor`](MANAGEMENT.TOKEN);
|
||||||
|
|
||||||
return this.$store.getters[`${ inStore }/schemaFor`](MANAGEMENT.TOKEN);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
apiKeys() {
|
principal() {
|
||||||
|
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addKey() {
|
||||||
|
this.$router.push({ path: 'account/create-key' });
|
||||||
|
},
|
||||||
|
async calcCanChangePassword() {
|
||||||
|
if (!this.$store.getters['auth/enabled']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.principal.provider === 'local') {
|
||||||
|
return !!this.principal.loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await this.$store.dispatch('rancher/findAll', {
|
||||||
|
type: NORMAN.USER,
|
||||||
|
opt: { url: '/v3/users', filter: { me: true } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users && users.length === 1) {
|
||||||
|
return !!users[0].username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async fetchApiKeys() {
|
||||||
|
const rows = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.TOKEN, opt: { force: true } });
|
||||||
|
|
||||||
// Filter out tokens that are not API Keys
|
// Filter out tokens that are not API Keys
|
||||||
const isApiKey = (key) => {
|
const isApiKey = (key) => {
|
||||||
const labels = key.metadata?.labels;
|
const labels = key.metadata?.labels;
|
||||||
|
|
@ -48,48 +77,65 @@ export default {
|
||||||
return kind !== 'session' && !key.current;
|
return kind !== 'session' && !key.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
return !this.rows ? [] : this.rows.filter(isApiKey);
|
return !rows ? [] : rows.filter(isApiKey);
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
methods: {
|
|
||||||
addKey() {
|
|
||||||
this.$router.push({ path: 'account/create-key' });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Loading v-if="$fetchState.pending" />
|
<Loading v-if="$fetchState.pending" />
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<header>
|
<h1 v-t="'accountAndKeys.title'" />
|
||||||
<div class="title">
|
<section class="account">
|
||||||
<h1 v-t="'account.apiKeys.title'" class="m-0"></h1>
|
<h4 v-t="'accountAndKeys.account.title'" />
|
||||||
</div>
|
<div class="content">
|
||||||
<div class="actions-container">
|
<div class="col mt-10">
|
||||||
<div class="actions">
|
<div><t k="accountAndKeys.account.name" />: {{ principal.name }}</div>
|
||||||
<button class="btn role-primary" @click="addKey">
|
<div><t k="accountAndKeys.account.username" />: {{ principal.loginName }}</div>
|
||||||
{{ t('account.apiKeys.add.label') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="canChangePassword"
|
||||||
|
type="button"
|
||||||
|
class="btn role-secondary"
|
||||||
|
@click="$refs.promptChangePassword.show(true)"
|
||||||
|
>
|
||||||
|
{{ t("accountAndKeys.account.change") }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<PromptChangePassword ref="promptChangePassword" />
|
||||||
<ResourceTable
|
</section>
|
||||||
:schema="schema"
|
|
||||||
:rows="apiKeys"
|
<section>
|
||||||
:headers="headers"
|
<h4 v-t="'accountAndKeys.keys.title'" />
|
||||||
key-field="id"
|
|
||||||
:search="false"
|
<!-- account.apiKeys.title -->
|
||||||
:row-actions="true"
|
<!-- account.apiKeys.add.label -->
|
||||||
:table-actions="true"
|
<!-- -->
|
||||||
/>
|
<button class="btn role-primary" @click="addKey">
|
||||||
|
{{ t('account.apiKeys.add.label') }}
|
||||||
|
</button>
|
||||||
|
<ResourceTable
|
||||||
|
:schema="apiKeySchema"
|
||||||
|
:rows="apiKeys"
|
||||||
|
:headers="apiKeyheaders"
|
||||||
|
key-field="id"
|
||||||
|
:search="false"
|
||||||
|
:row-actions="true"
|
||||||
|
:table-actions="true"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang='scss' scoped>
|
||||||
hr {
|
section {
|
||||||
margin: 20px 0;
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account {
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<div v-if="!kialiUrl" class="disabled-msg">
|
||||||
|
<span v-html="t('istio.links.disabled', {app: 'Kiali'})" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{'disabled':!jaegerUrl}" class="box link-container">
|
<div :class="{'disabled':!jaegerUrl}" class="box link-container">
|
||||||
<span
|
<span
|
||||||
|
|
@ -127,6 +130,9 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<div v-if="!jaegerUrl" class="disabled-msg">
|
||||||
|
<span v-html="t('istio.links.disabled', {app: 'Jaeger'})" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,6 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ function SteveFactory(namespace, baseUrl) {
|
||||||
},
|
},
|
||||||
types: {},
|
types: {},
|
||||||
socket: null,
|
socket: null,
|
||||||
|
queue: [],
|
||||||
wantSocket: false,
|
wantSocket: false,
|
||||||
pendingSends: [],
|
pendingSends: [],
|
||||||
started: [],
|
started: [],
|
||||||
|
|
|
||||||
|
|
@ -777,7 +777,6 @@ export default {
|
||||||
if ( !opt.url ) {
|
if ( !opt.url ) {
|
||||||
opt.url = this.actionLinkFor(actionName);
|
opt.url = this.actionLinkFor(actionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
opt.method = 'post';
|
opt.method = 'post';
|
||||||
opt.data = body;
|
opt.data = body;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import { open, popupWindowOptions } from '@/utils/window';
|
||||||
import {
|
import {
|
||||||
BACK_TO, SPA, AUTH_TEST, _FLAGGED, GITHUB_SCOPE, GITHUB_NONCE, GITHUB_REDIRECT
|
BACK_TO, SPA, AUTH_TEST, _FLAGGED, GITHUB_SCOPE, GITHUB_NONCE, GITHUB_REDIRECT
|
||||||
} from '@/config/query-params';
|
} from '@/config/query-params';
|
||||||
import { BASE_SCOPES } from '@/store/github';
|
|
||||||
|
export const BASE_SCOPES = { github: ['read:org'], googleoauth: ['email'] };
|
||||||
|
|
||||||
const KEY = 'rc_nonce';
|
const KEY = 'rc_nonce';
|
||||||
|
|
||||||
|
|
@ -143,7 +144,7 @@ export const actions = {
|
||||||
const fromQuery = unescape(parseUrl(redirectUrl).query?.[GITHUB_SCOPE] || '');
|
const fromQuery = unescape(parseUrl(redirectUrl).query?.[GITHUB_SCOPE] || '');
|
||||||
const scopes = fromQuery.split(/[, ]+/).filter(x => !!x);
|
const scopes = fromQuery.split(/[, ]+/).filter(x => !!x);
|
||||||
|
|
||||||
addObjects(scopes, BASE_SCOPES);
|
addObjects(scopes, BASE_SCOPES[provider]);
|
||||||
|
|
||||||
if ( opt.scopes ) {
|
if ( opt.scopes ) {
|
||||||
addObjects(scopes, opt.scopes);
|
addObjects(scopes, opt.scopes);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { GITHUB_REPOS, GITHUB_SCOPES, _DATE } from '@/config/local-storage';
|
||||||
|
|
||||||
const API_BASE = 'https://api.github.com/';
|
const API_BASE = 'https://api.github.com/';
|
||||||
|
|
||||||
export const BASE_SCOPES = ['read:org'];
|
|
||||||
export const EXTENDED_SCOPES = ['repo'];
|
export const EXTENDED_SCOPES = ['repo'];
|
||||||
|
|
||||||
export const DOCKERFILE = /^Dockerfile(\..*)?$/i;
|
export const DOCKERFILE = /^Dockerfile(\..*)?$/i;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue