UI: PKI stub configure page (#18349)

This commit is contained in:
Chelsea Shaw 2022-12-14 11:57:03 -06:00 committed by GitHub
parent fccc90ce75
commit 23a156122f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 242 additions and 54 deletions

View File

@ -0,0 +1,45 @@
<div class="box is-bottomless is-fullwidth is-marginless">
<div class="columns">
{{#each this.configTypes as |option|}}
<div class="column is-flex">
<label for={{option.key}} class="box-label is-column {{if (eq this.configType option.key) 'is-selected'}}">
<div>
<h3 class="box-label-header title is-6">
<Icon @size="24" @name={{option.icon}} />
{{option.label}}
</h3>
<p class="help has-text-grey-dark">
{{option.description}}
</p>
</div>
<div>
<RadioButton
id={{option.key}}
name="pki-config-type"
@value={{this.configType}}
@groupValue={{this.replicationMode}}
@onChange={{fn (mut this.configType) option.key}}
data-test-pki-config-option={{option.key}}
/>
<label for={{option.key}}></label>
</div>
</label>
</div>
{{/each}}
</div>
{{#if this.configType}}
{{! TODO: Forms }}
{{else}}
<EmptyState @title="Choose an option" @message="To see configuration options, choose your desired output above." />
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="button" class="button is-primary" disabled={{true}} data-test-pki-config-save>
Done
</button>
<LinkTo @route="overview" class="button has-left-margin-s" data-test-pki-config-cancel>
Cancel
</LinkTo>
</div>
</div>
{{/if}}
</div>

View File

@ -0,0 +1,38 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
/**
* @module PkiConfigureForm
* PkiConfigureForm components are used to configure a PKI engine mount.
*
*/
export default class PkiConfigureForm extends Component {
@tracked configType = '';
get configTypes() {
return [
{
key: 'import',
icon: 'download',
label: 'Import a CA',
description:
'Import CA information via a PEM file containing the CA certificate and any private keys, concatenated together, in any order.',
},
{
key: 'generate-root',
icon: 'file-plus',
label: 'Generate root',
description:
'Generates a new self-signed CA certificate and private key. This generated root will sign its own CRL.',
},
{
key: 'generate-csr',
icon: 'files',
label: 'Generate intermediate CSR',
description:
'Generate a new CSR for signing, optionally generating a new private key. No new issuer is created by this call.',
},
];
}
}

View File

@ -3,6 +3,7 @@ import buildRoutes from 'ember-engines/routes';
export default buildRoutes(function () {
this.route('overview');
this.route('configuration', function () {
this.route('index', { path: '/' });
this.route('tidy');
this.route('create');
this.route('edit');

View File

@ -1,3 +1,16 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class PkiConfigurationCreateRoute extends Route {}
export default class PkiConfigurationCreateRoute extends Route {
@service secretMountPath;
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'configure' },
];
}
}

View File

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ConfigurationIndexRoute extends Route {}

View File

@ -1,3 +1,42 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default class PkiOverviewRoute extends Route {}
export default class PkiOverviewRoute extends Route {
@service secretMountPath;
@service auth;
@service store;
get win() {
return this.window || window;
}
hasConfig() {
const endpoint = `${this.win.origin}/v1/${this.secretMountPath.currentPath}/issuers?list=true`;
return this.auth
.ajax(endpoint, 'GET', {})
.then(() => true)
.catch(() => false);
}
async model() {
return hash({
hasConfig: this.hasConfig(),
engine: this.store
.query('secret-engine', {
path: this.secretMountPath.currentPath,
})
.then((model) => {
if (model) {
return model.get('firstObject');
}
}),
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
const backend = this.secretMountPath.currentPath || 'pki';
controller.breadcrumbs = [{ label: 'secrets', route: 'secrets', linkExternal: true }, { label: backend }];
}
}

View File

@ -1,39 +1 @@
<SecretListHeader
@model={{this.model}}
@backendCrumb={{hash
label=this.model.id
text=this.model.id
path="vault.cluster.secrets.backend.list-root"
model=this.model.id
}}
@isEngine={{true}}
/>
<Toolbar>
<ToolbarActions>
<ToolbarLink @route="configuration.tidy">
Tidy
</ToolbarLink>
<ToolbarLink @route="configuration.edit">
Edit configuration
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<div class="form-section">
<label class="title has-padding-top is-5">
URLs
</label>
</div>
<div class="form-section">
<label class="title has-padding-top is-5">
CRL
</label>
</div>
<div class="form-section">
<label class="title has-padding-top is-5">
Mount Configuration
</label>
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{! ARG TODO component and empty state }}
</div>
</div>
{{outlet}}

View File

@ -1 +1,12 @@
route: pki.configuration.create
<PageHeader as |p|>
<p.top>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-pki-key-page-title>
Configure PKI
</h1>
</p.levelLeft>
</PageHeader>
<PkiConfigureForm />

View File

@ -0,0 +1,40 @@
<SecretListHeader
@model={{this.model}}
@backendCrumb={{hash
label=this.model.id
text=this.model.id
path="vault.cluster.secrets.backend.list-root"
model=this.model.id
}}
@isEngine={{true}}
/>
<Toolbar>
<ToolbarActions>
<ToolbarLink @route="configuration.tidy">
Tidy
</ToolbarLink>
<ToolbarLink @route="configuration.edit">
Edit configuration
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<div class="form-section">
<label class="title has-padding-top is-5">
URLs
</label>
</div>
<div class="form-section">
<label class="title has-padding-top is-5">
CRL
</label>
</div>
<div class="form-section">
<label class="title has-padding-top is-5">
Mount Configuration
</label>
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{! ARG TODO component and empty state }}
</div>
</div>
{{outlet}}

View File

@ -1,23 +1,29 @@
<SecretListHeader
@model={{this.model}}
@model={{this.model.engine}}
@backendCrumb={{hash
label=this.model.id
text=this.model.id
label=this.model.engine.id
text=this.model.engine.id
path="vault.cluster.secrets.backend.list-root"
model=this.model.id
model=this.model.engine.id
}}
@isEngine={{true}}
/>
<Toolbar>
<ToolbarActions>
{{#unless this.model.hasConfig}}
<ToolbarLink @route="configuration.create">
Configure PKI
</ToolbarLink>
{{/unless}}
</ToolbarActions>
</Toolbar>
<EmptyState @title="PKI not configured" @message="This PKI mount hasnt yet been configured with a certificate issuer.">
<LinkTo @route="configuration.create" @model={{this.model}}>
{{#if this.model.hasConfig}}
{{! TODO show overview items }}
{{else}}
<EmptyState @title="PKI not configured" @message="This PKI mount hasnt yet been configured with a certificate issuer.">
<LinkTo @route="configuration.create" @model={{this.model.engine}}>
Configure PKI
</LinkTo>
</EmptyState>
</EmptyState>
{{/if}}

View File

@ -0,0 +1,30 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { click, render } from '@ember/test-helpers';
import { setupEngine } from 'ember-engines/test-support';
import { hbs } from 'ember-cli-htmlbars';
const SELECTORS = {
option: '[data-test-pki-config-option]',
optionByKey: (key) => `[data-test-pki-config-option="${key}"]`,
cancelButton: '[data-test-pki-config-cancel]',
saveButton: '[data-test-pki-config-save]',
};
module('Integration | Component | pki-configure-form', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
test('it renders', async function (assert) {
await render(hbs`<PkiConfigureForm />`, { owner: this.engine });
assert.dom(SELECTORS.option).exists({ count: 3 }, 'Three configuration options are shown');
assert.dom(SELECTORS.cancelButton).exists('Cancel link is shown');
assert.dom(SELECTORS.saveButton).isDisabled('Done button is disabled');
await click(SELECTORS.optionByKey('import'));
assert.dom(SELECTORS.optionByKey('import')).isChecked('Selected item is checked');
await click(SELECTORS.optionByKey('generate-csr'));
assert.dom(SELECTORS.optionByKey('generate-csr')).isChecked('Selected item is checked');
});
});