Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEnrique Alcántara <ealcantara@gitlab.com>2019-05-02 17:17:03 +0300
committerPhil Hughes <me@iamphill.com>2019-05-02 17:17:03 +0300
commitbf229a6c632709ef7c69a7033eadfd74f5ddcc97 (patch)
tree876b70f0ddb0e111199a685dd53fb71adeba5552 /app/assets
parentd6aa8a0553e30ef2d0d303fc79515cd5cf3ba1b9 (diff)
Uninstall application confirm modal component
- Vue confirmation modal implementation - CSS tweaks for modal default height
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js23
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue71
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue57
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue23
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue66
-rw-r--r--app/assets/javascripts/clusters/constants.js7
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js36
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js4
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js16
-rw-r--r--app/assets/stylesheets/framework/modal.scss2
10 files changed, 265 insertions, 40 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 8461e01de7b..561b6bdd9f1 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -132,6 +132,7 @@ export default class Clusters {
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
+ eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
}
removeListeners() {
@@ -141,6 +142,7 @@ export default class Clusters {
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
+ eventHub.$off('uninstallApplication');
}
initPolling() {
@@ -249,14 +251,13 @@ export default class Clusters {
}
}
- installApplication(data) {
- const appId = data.id;
+ installApplication({ id: appId, params }) {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
this.store.installApplication(appId);
- return this.service.installApplication(appId, data.params).catch(() => {
+ return this.service.installApplication(appId, params).catch(() => {
this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
@@ -266,6 +267,22 @@ export default class Clusters {
});
}
+ uninstallApplication({ id: appId }) {
+ this.store.updateAppProperty(appId, 'requestReason', null);
+ this.store.updateAppProperty(appId, 'statusReason', null);
+
+ this.store.uninstallApplication(appId);
+
+ return this.service.uninstallApplication(appId).catch(() => {
+ this.store.notifyUninstallFailure(appId);
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin uninstalling failed'),
+ );
+ });
+ }
+
upgradeApplication(data) {
const appId = data.id;
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index a351916942e..5f7675bb432 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,12 +1,13 @@
<script>
/* eslint-disable vue/require-default-prop */
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import UninstallApplicationButton from './uninstall_application_button.vue';
+import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
import { APPLICATION_STATUS } from '../constants';
@@ -17,6 +18,10 @@ export default {
TimeagoTooltip,
GlLink,
UninstallApplicationButton,
+ UninstallApplicationConfirmationModal,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
id: {
@@ -94,6 +99,16 @@ export default {
required: false,
default: false,
},
+ uninstallFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ uninstallSuccessful: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
updateAcknowledged: {
type: Boolean,
required: false,
@@ -170,10 +185,21 @@ export default {
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
+ hasError() {
+ return this.installFailed || this.uninstallFailed;
+ },
generalErrorDescription() {
- return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
- title: this.title,
- });
+ let errorDescription;
+
+ if (this.installFailed) {
+ errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
+ } else if (this.uninstallFailed) {
+ errorDescription = s__(
+ 'ClusterIntegration|Something went wrong while uninstalling %{title}',
+ );
+ }
+
+ return sprintf(errorDescription, { title: this.title });
},
versionLabel() {
if (this.updateFailed) {
@@ -214,13 +240,23 @@ export default {
// AND new upgrade is unavailable AND version information is present.
return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
},
+ uninstallSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
+ title: this.title,
+ });
+ },
},
watch: {
- updateSuccessful() {
- if (this.updateSuccessful) {
+ updateSuccessful(updateSuccessful) {
+ if (updateSuccessful) {
this.$toast.show(this.upgradeSuccessDescription);
}
},
+ uninstallSuccessful(uninstallSuccessful) {
+ if (uninstallSuccessful) {
+ this.$toast.show(this.uninstallSuccessDescription);
+ }
+ },
},
methods: {
installClicked() {
@@ -235,6 +271,11 @@ export default {
params: this.installApplicationRequestParams,
});
},
+ uninstallConfirmed() {
+ eventHub.$emit('uninstallApplication', {
+ id: this.id,
+ });
+ },
},
};
</script>
@@ -271,10 +312,7 @@ export default {
<span v-else class="js-cluster-application-title">{{ title }}</span>
</strong>
<slot name="description"></slot>
- <div
- v-if="installFailed || isUnknownStatus"
- class="cluster-application-error text-danger prepend-top-10"
- >
+ <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10">
<p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }}
</p>
@@ -325,9 +363,9 @@ export default {
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
- <a :href="manageLink" :class="{ disabled: disabled }" class="btn">
- {{ manageButtonLabel }}
- </a>
+ <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
+ manageButtonLabel
+ }}</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
@@ -340,8 +378,15 @@ export default {
/>
<uninstall-application-button
v-if="displayUninstallButton"
+ v-gl-modal-directive="'uninstall-' + id"
+ :status="status"
class="js-cluster-application-uninstall-button"
/>
+ <uninstall-application-confirmation-modal
+ :application="id"
+ :application-title="title"
+ @confirm="uninstallConfirmed()"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index dfc2069f131..287bdbcf873 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -240,6 +240,9 @@ export default {
:request-reason="applications.helm.requestReason"
:installed="applications.helm.installed"
:install-failed="applications.helm.installFailed"
+ :uninstallable="applications.helm.uninstallable"
+ :uninstall-successful="applications.helm.uninstallSuccessful"
+ :uninstall-failed="applications.helm.uninstallFailed"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
@@ -269,6 +272,9 @@ export default {
:request-reason="applications.ingress.requestReason"
:installed="applications.ingress.installed"
:install-failed="applications.ingress.installFailed"
+ :uninstallable="applications.ingress.uninstallable"
+ :uninstall-successful="applications.ingress.uninstallSuccessful"
+ :uninstall-failed="applications.ingress.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
@@ -312,9 +318,9 @@ export default {
generated endpoint in order to access
your application after it has been deployed.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</div>
@@ -324,9 +330,9 @@ export default {
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</template>
<template v-if="!ingressInstalled">
@@ -345,6 +351,9 @@ export default {
:installed="applications.cert_manager.installed"
:install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
+ :uninstallable="applications.cert_manager.uninstallable"
+ :uninstall-successful="applications.cert_manager.uninstallSuccessful"
+ :uninstall-failed="applications.cert_manager.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
@@ -352,9 +361,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
- <label for="cert-manager-issuer-email">
- {{ s__('ClusterIntegration|Issuer Email') }}
- </label>
+ <label for="cert-manager-issuer-email">{{
+ s__('ClusterIntegration|Issuer Email')
+ }}</label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
@@ -391,6 +400,9 @@ export default {
:request-reason="applications.prometheus.requestReason"
:installed="applications.prometheus.installed"
:install-failed="applications.prometheus.installFailed"
+ :uninstallable="applications.prometheus.uninstallable"
+ :uninstall-successful="applications.prometheus.uninstallSuccessful"
+ :uninstall-failed="applications.prometheus.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
@@ -411,6 +423,9 @@ export default {
:install-failed="applications.runner.installFailed"
:update-successful="applications.runner.updateSuccessful"
:update-failed="applications.runner.updateFailed"
+ :uninstallable="applications.runner.uninstallable"
+ :uninstall-successful="applications.runner.uninstallSuccessful"
+ :uninstall-failed="applications.runner.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
@@ -434,6 +449,9 @@ export default {
:request-reason="applications.jupyter.requestReason"
:installed="applications.jupyter.installed"
:install-failed="applications.jupyter.installFailed"
+ :uninstallable="applications.jupyter.uninstallable"
+ :uninstall-successful="applications.jupyter.uninstallSuccessful"
+ :uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
@@ -474,9 +492,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</div>
</template>
@@ -494,6 +512,9 @@ export default {
:installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
+ :uninstallable="applications.knative.uninstallable"
+ :uninstall-successful="applications.knative.uninstallSuccessful"
+ :uninstall-failed="applications.knative.uninstallFailed"
:disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
@@ -505,9 +526,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
<br />
</span>
@@ -572,9 +593,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
<p
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
index 30918d1d115..ef4bcbe14dd 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
@@ -1,14 +1,33 @@
<script>
-// TODO: Implement loading button component
import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
LoadingButton,
},
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ disabled() {
+ return [UNINSTALLING, UPDATING].includes(this.status);
+ },
+ loading() {
+ return this.status === UNINSTALLING;
+ },
+ label() {
+ return this.loading ? this.__('Uninstalling') : this.__('Uninstall');
+ },
+ },
};
</script>
<template>
- <loading-button @click="$emit('click')" />
+ <loading-button :label="label" :disabled="disabled" :loading="loading" />
</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
new file mode 100644
index 00000000000..80ba2f22198
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
+
+const CUSTOM_APP_WARNING_TEXT = {
+ [INGRESS]: s__(
+ 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
+ ),
+ [CERT_MANAGER]: s__(
+ 'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.',
+ ),
+ [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
+ [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
+ [KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'),
+ [JUPYTER]: '',
+};
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ application: {
+ type: String,
+ required: true,
+ },
+ applicationTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
+ appTitle: this.applicationTitle,
+ });
+ },
+ warningText() {
+ return sprintf(
+ s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
+ {
+ appTitle: this.applicationTitle,
+ },
+ );
+ },
+ customAppWarningText() {
+ return CUSTOM_APP_WARNING_TEXT[this.application];
+ },
+ modalId() {
+ return `uninstall-${this.application}`;
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ ok-variant="danger"
+ cancel-variant="light"
+ :ok-title="title"
+ :modal-id="modalId"
+ :title="title"
+ @ok="$emit('confirm')"
+ >{{ warningText }} {{ customAppWarningText }}</gl-modal
+ >
+</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 48dbce9676e..8fd752092c9 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -28,16 +28,23 @@ export const APPLICATION_STATUS = {
export const APPLICATION_INSTALLED_STATUSES = [
APPLICATION_STATUS.INSTALLED,
APPLICATION_STATUS.UPDATING,
+ APPLICATION_STATUS.UNINSTALLING,
];
// These are only used client-side
export const UPDATE_EVENT = 'update';
export const INSTALL_EVENT = 'install';
+export const UNINSTALL_EVENT = 'uninstall';
+export const HELM = 'helm';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
+export const PROMETHEUS = 'prometheus';
+
+export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
+
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index aafb2350ae4..14b80a116a7 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -1,4 +1,4 @@
-import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '../constants';
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
const {
NO_STATUS,
@@ -11,6 +11,8 @@ const {
UPDATING,
UPDATED,
UPDATE_ERRORED,
+ UNINSTALLING,
+ UNINSTALL_ERRORED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
@@ -52,6 +54,15 @@ const applicationStateMachine = {
updateFailed: true,
},
},
+ [UNINSTALLING]: {
+ target: UNINSTALLING,
+ },
+ [UNINSTALL_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ uninstallFailed: true,
+ },
+ },
},
},
[NOT_INSTALLABLE]: {
@@ -97,6 +108,13 @@ const applicationStateMachine = {
updateSuccessful: false,
},
},
+ [UNINSTALL_EVENT]: {
+ target: UNINSTALLING,
+ effects: {
+ uninstallFailed: false,
+ uninstallSuccessful: false,
+ },
+ },
},
},
[UPDATING]: {
@@ -116,6 +134,22 @@ const applicationStateMachine = {
},
},
},
+ [UNINSTALLING]: {
+ on: {
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ effects: {
+ uninstallSuccessful: true,
+ },
+ },
+ [UNINSTALL_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ uninstallFailed: true,
+ },
+ },
+ },
+ },
};
/**
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index dea33ac44c5..01f3732de7e 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -29,6 +29,10 @@ export default class ClusterService {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
+ uninstallApplication(appId, params) {
+ return axios.delete(this.appInstallEndpointMap[appId], params);
+ }
+
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index c2e30960659..1b4d7e8372c 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -10,6 +10,7 @@ import {
APPLICATION_STATUS,
INSTALL_EVENT,
UPDATE_EVENT,
+ UNINSTALL_EVENT,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
@@ -21,6 +22,9 @@ const applicationInitialState = {
requestReason: null,
installed: false,
installFailed: false,
+ uninstallable: false,
+ uninstallFailed: false,
+ uninstallSuccessful: false,
};
export default class ClusterStore {
@@ -116,6 +120,14 @@ export default class ClusterStore {
this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
}
+ uninstallApplication(appId) {
+ this.handleApplicationEvent(appId, UNINSTALL_EVENT);
+ }
+
+ notifyUninstallFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
+ }
+
handleApplicationEvent(appId, event) {
const currentAppState = this.state.applications[appId];
@@ -141,6 +153,7 @@ export default class ClusterStore {
status_reason: statusReason,
version,
update_available: upgradeAvailable,
+ can_uninstall: uninstallable,
} = serverAppEntry;
const currentApplicationState = this.state.applications[appId] || {};
const nextApplicationState = transitionApplicationState(currentApplicationState, status);
@@ -150,8 +163,7 @@ export default class ClusterStore {
...nextApplicationState,
statusReason,
installed: isApplicationInstalled(nextApplicationState.status),
- // Make sure uninstallable is always false until this feature is unflagged
- uninstallable: false,
+ uninstallable,
};
if (appId === INGRESS) {
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 53222a2bd4d..323a3dbecd5 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -34,10 +34,10 @@
.modal-body {
background-color: $modal-body-bg;
line-height: $line-height-base;
- min-height: $modal-body-height;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
text-align: left;
+ white-space: normal;
.form-actions {
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};