Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pub/terrareg
Show changes
Commits on Source (35)
Showing
with 526 additions and 203 deletions
# Changelog
# [3.6.0](https://gitlab.dockstudios.co.uk/pub/terrareg/compare/v3.5.0...v3.6.0) (2024-06-13)
### Bug Fixes
* Disable logging around disabling OIDC, unless debug is enabled ([ef0885a](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/ef0885aa787d84c49332eca08be755754c20ba1c)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Handle multi-line types in new display of input variables ([fdcbbaf](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/fdcbbaf173ea00243c6d93ae66c79892c85f07e3)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Improve formatting of tables in inputs tab ([08cc3e9](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/08cc3e91adf39abb5059a87061f03cc69f6b6655)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Improve handling of multi-line default values in input variables for module provider ([f041908](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/f041908ff2270bb0d5f1818892dbb0172ac3fb40)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Split required and optional inputs in inputs tab ([f7ef44c](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/f7ef44c7eca02ea526a29aff14a6db7756ce4b71)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
### Features
* Add support for rendering input/output descriptions as markdown. ([9171cb7](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/9171cb75b55bbbf668d3e1b386d95b8b7e50479b)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Add support for switching between table and expanded view in inputs and outputs ([cdd9fda](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/cdd9fdaba631be49ac57620253ad4475a28710e6)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Add UI support for markdown in input/output descriptions ([4f49257](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/4f49257eb39b9ec979a3d4ed1c11e4cb026822c4)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Allow setting default input/output UI view type system-wide ([8375e1d](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/8375e1d998893ef43bcd9e742984065cc2d32218)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Update output table to no longer use a table and use full page content for description ([a710e9b](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/a710e9b07216269a84811117115ff3e0fb5e008a)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
* Update usage builder variables API endpoint to support HTML descriptions ([d743030](https://gitlab.dockstudios.co.uk/pub/terrareg/commit/d74303030ddde49dc7dccced0959b42a6913a5d4)), closes [#527](https://gitlab.dockstudios.co.uk/pub/terrareg/issues/527)
# [3.5.0](https://gitlab.dockstudios.co.uk/pub/terrareg/compare/v3.4.2...v3.5.0) (2024-06-03)
......
......@@ -750,28 +750,25 @@ Interface to obtain list of providers for module.
Return list of modules in namespace
## ApiTerraregModuleProviderDetails
## ApiTerraregModuleVersionDetails
`/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>`
Interface to obtain module provider details.
#### GET
Return details about module version.
## ApiTerraregModuleVersionDetails
`/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>/<string:version>`
Interface to obtain module version details.
Interface to obtain module provider/version details.
#### GET
Return details about module version.
##### Arguments
| Argument | Location (JSON POST body or query string argument) | Type | Required | Default | Help |
|----------|----------------------------------------------------|------|----------|---------|------|
| target_terraform_version | args | str | False | `None` | Provide terraform version to show compatibility with search results. |
| output | args | str | False | `md` | Variable/Output description format, either "html" or "md" |
## ApiTerraregModuleProviderVersions
......@@ -892,6 +889,12 @@ Provide variable template for module version.
#### GET
Return variable template.
##### Arguments
| Argument | Location (JSON POST body or query string argument) | Type | Required | Default | Help |
|----------|----------------------------------------------------|------|----------|---------|------|
| output | args | str | False | `md` | Variable/Output description format, either "html" or "md" |
## ApiTerraregModuleVersionFile
......
......@@ -285,6 +285,19 @@ Default version of Terraform that will be used to extract module, if terraform r
Default: `1.3.6`
### DEFAULT_UI_DETAILS_VIEW
Default view type in UI for inputs and outputs
Set to one of the following:
* table - show the inputs and outputs in a table
* expanded - shows the inputs and outputs as a list, more suitable for long descriptions and descriptions containing markdown tables
Default: `table`
### DELETE_EXTERNALLY_HOSTED_ARTIFACTS
......
......@@ -26,6 +26,12 @@ class ModuleHostingMode(Enum):
ENFORCE = "enforce"
class DefaultUiInputOutputView(Enum):
"""Default input/output view in UI"""
TABLE = "table"
EXPANDED = "expanded"
class Config:
@property
......@@ -210,6 +216,17 @@ class Config:
"""
return self.convert_boolean(os.environ.get('DISABLE_ANALYTICS', 'False'))
@property
def DEFAULT_UI_DETAILS_VIEW(self):
"""
Default view type in UI for inputs and outputs
Set to one of the following:
* table - show the inputs and outputs in a table
* expanded - shows the inputs and outputs as a list, more suitable for long descriptions and descriptions containing markdown tables
"""
return DefaultUiInputOutputView(os.environ.get('DEFAULT_UI_DETAILS_VIEW', DefaultUiInputOutputView.TABLE.value).lower())
@property
def ALLOW_FORCEFUL_MODULE_PROVIDER_REDIRECT_DELETION(self):
"""
......
......@@ -3298,13 +3298,33 @@ module "{self.module_version.module_provider.module.name}" {{
return content
return None
def get_terraform_inputs(self):
def get_terraform_inputs(self, html: bool=False):
"""Obtain module inputs"""
return self.get_module_specs().get('inputs', [])
def get_terraform_outputs(self):
inputs = self.get_module_specs().get('inputs', [])
# Rewrite variable/output descriptions to use markdown
for input_ in inputs:
description = input_.get("description")
if description:
if html:
description = convert_markdown_to_html(file_name="", markdown_html=description)
# Always sanitise HTML
description = sanitise_html_content(text=description, allow_markdown_html=True)
input_["description"] = description
return inputs
def get_terraform_outputs(self, html: bool=False):
"""Obtain module inputs"""
return self.get_module_specs().get('outputs', [])
outputs = self.get_module_specs().get('outputs', [])
# Rewrite variable/output descriptions to use markdown
for output in outputs:
description = output.get("description")
if description:
if html:
description = convert_markdown_to_html(file_name="", markdown_html=description)
# Always sanitise HTML
description = sanitise_html_content(text=description, allow_markdown_html=True)
output["description"] = description
return outputs
def get_terraform_resources(self):
"""Obtain module resources."""
......@@ -3473,14 +3493,14 @@ module "{self.module_version.module_provider.module.name}" {{
return requirement["version"]
return None
def get_api_module_specs(self):
def get_api_module_specs(self, html: bool=False):
"""Return module specs for API."""
return {
"path": self.path,
"readme": self.get_readme_content(),
"empty": False,
"inputs": self.get_terraform_inputs(),
"outputs": self.get_terraform_outputs(),
"inputs": self.get_terraform_inputs(html=html),
"outputs": self.get_terraform_outputs(html=html),
"dependencies": self.get_terraform_dependencies(),
"provider_dependencies": self.get_terraform_provider_dependencies(),
"resources": self.get_terraform_resources()
......@@ -3675,8 +3695,7 @@ class ModuleVersion(TerraformSpecsObject):
"""Return registry path ID (with excludes version)."""
return self._module_provider.id
@property
def variable_template(self):
def get_variable_template(self, html: bool=False):
"""Return variable template for module version."""
raw_json = Database.decode_blob(self._get_db_row()['variable_template'])
variables = json.loads(raw_json) if raw_json else []
......@@ -3685,7 +3704,7 @@ class ModuleVersion(TerraformSpecsObject):
for variable in variables:
variable['required'] = variable.get('required', True)
variable['type'] = variable.get('type', 'text')
variable['additional_help'] = variable.get('additional_help', '')
variable['additional_help'] = sanitise_html_content(variable.get('additional_help', ''))
variable['quote_value'] = variable.get('quote_value', True)
variable['default_value'] = variable.get('default_value', None)
......@@ -3695,7 +3714,7 @@ class ModuleVersion(TerraformSpecsObject):
variables = []
if terrareg.config.Config().AUTOGENERATE_USAGE_BUILDER_VARIABLES:
for input_variable in self.get_terraform_inputs():
for input_variable in self.get_terraform_inputs(html=html):
if input_variable['name'] not in [v['name'] for v in variables]:
converted_type = 'text'
quote_value = True
......@@ -4029,7 +4048,7 @@ class ModuleVersion(TerraformSpecsObject):
self._module_provider.calculate_latest_version().version == self.version):
self._module_provider.update_attributes(latest_version_id=self.pk)
def get_api_outline(self, target_terraform_version=None):
def get_api_outline(self, target_terraform_version: Optional[str]=None):
"""Return dict of basic version details for API response."""
row = self._get_db_row()
api_outline = self._module_provider.get_api_outline()
......@@ -4057,18 +4076,18 @@ class ModuleVersion(TerraformSpecsObject):
module_version=self
)
def get_api_details(self, target_terraform_version=None):
"""Return dict of version details for API response."""#
def get_api_details(self, target_terraform_version: Optional[str]=None, html: bool=False):
"""Return dict of version details for API response."""
api_details = self._module_provider.get_api_details()
api_details.update(self.get_api_outline(target_terraform_version=target_terraform_version))
api_details.update({
"root": self.get_api_module_specs(),
"root": self.get_api_module_specs(html=html),
"submodules": [sm.get_api_module_specs() for sm in self.get_submodules()],
"providers": [p.name for p in self._module_provider._module.get_providers()]
})
return api_details
def get_terrareg_api_details(self, request_domain, target_terraform_version=None):
def get_terrareg_api_details(self, request_domain, target_terraform_version: Optional[str]=None, html: bool=False):
"""Return dict of version details with additional attributes used by terrareg UI."""
# Obtain module provider terrareg API details
api_details = self._module_provider.get_terrareg_api_details()
......@@ -4078,7 +4097,7 @@ class ModuleVersion(TerraformSpecsObject):
versions = api_details['versions']
# Update with API details from the module version
api_details.update(self.get_api_details(target_terraform_version=target_terraform_version))
api_details.update(self.get_api_details(target_terraform_version=target_terraform_version, html=html))
tab_files = [module_version_file.path for module_version_file in self.module_version_files]
additional_module_tabs = json.loads(terrareg.config.Config().ADDITIONAL_MODULE_TABS)
......
......@@ -519,13 +519,10 @@ class Server(BaseHandler):
ApiTerraregModuleProviders,
'/v1/terrareg/modules/<string:namespace>/<string:name>'
)
self._api.add_resource(
ApiTerraregModuleProviderDetails,
'/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>'
)
self._api.add_resource(
ApiTerraregModuleVersionDetails,
'/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>/<string:version>'
'/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>',
'/v1/terrareg/modules/<string:namespace>/<string:name>/<string:provider>/<string:version>',
)
self._api.add_resource(
ApiTerraregModuleProviderVersions,
......
......@@ -36,7 +36,6 @@ from .terrareg_is_authenticated import ApiTerraregIsAuthenticated
from .terrareg_module_provider_analytics_token_versions import ApiTerraregModuleProviderAnalyticsTokenVersions
from .terrareg_module_provider_create import ApiTerraregModuleProviderCreate
from .terrareg_module_provider_delete import ApiTerraregModuleProviderDelete
from .terrareg_module_provider_details import ApiTerraregModuleProviderDetails
from .terrareg_module_provider_integrations import ApiTerraregModuleProviderIntegrations
from .terrareg_module_provider_settings import ApiTerraregModuleProviderSettings
from .terrareg_module_provider_versions import ApiTerraregModuleProviderVersions
......
......@@ -45,5 +45,6 @@ class ApiTerraregConfig(ErrorCatchingResource):
'SAML_LOGIN_TEXT': config.SAML2_LOGIN_TEXT,
'ADMIN_LOGIN_ENABLED': terrareg.auth.AdminApiKeyAuthMethod.is_enabled(),
'AUTO_CREATE_NAMESPACE': config.AUTO_CREATE_NAMESPACE,
'AUTO_CREATE_MODULE_PROVIDER': config.AUTO_CREATE_MODULE_PROVIDER
'AUTO_CREATE_MODULE_PROVIDER': config.AUTO_CREATE_MODULE_PROVIDER,
'DEFAULT_UI_DETAILS_VIEW': config.DEFAULT_UI_DETAILS_VIEW.value,
}
\ No newline at end of file
import urllib.parse
from flask import request
from flask_restful import reqparse
from terrareg.server.error_catching_resource import ErrorCatchingResource
import terrareg.auth_wrapper
class ApiTerraregModuleProviderDetails(ErrorCatchingResource):
"""Interface to obtain module provider details."""
method_decorators = [terrareg.auth_wrapper.auth_wrapper('can_access_read_api')]
def _get(self, namespace, name, provider):
"""Return details about module version."""
parser = reqparse.RequestParser()
parser.add_argument(
'target_terraform_version', type=str, location='args', default=None,
help='Provide terraform version to show compatibility with search results.'
)
args = parser.parse_args()
_, _, module_provider, error = self.get_module_provider_by_names(namespace, name, provider)
if error:
return error
# If a version exists, obtain the details for that
latest_version = module_provider.get_latest_version()
if latest_version is not None:
return latest_version.get_terrareg_api_details(
request_domain=urllib.parse.urlparse(request.base_url).hostname,
target_terraform_version=args.target_terraform_version
)
# Otherwise, return module provider details
return module_provider.get_terrareg_api_details()
......@@ -10,18 +10,30 @@ import terrareg.auth_wrapper
class ApiTerraregModuleVersionDetails(ErrorCatchingResource):
"""Interface to obtain module version details."""
"""Interface to obtain module provider/version details."""
method_decorators = [terrareg.auth_wrapper.auth_wrapper('can_access_read_api')]
def _get(self, namespace, name, provider, version=None):
"""Return details about module version."""
def _get_arg_parser(self) -> reqparse.RequestParser:
"""Return argument parser for GET method"""
parser = reqparse.RequestParser()
parser.add_argument(
'target_terraform_version', type=str, location='args', default=None,
help='Provide terraform version to show compatibility with search results.'
)
parser.add_argument(
'output',
type=str,
location='args',
default='md',
dest='output',
help='Variable/Output description format, either "html" or "md"'
)
return parser
def _get(self, namespace, name, provider, version=None):
"""Return details about module version."""
parser = self._get_arg_parser()
args = parser.parse_args()
_, _, module_provider, error = self.get_module_provider_by_names(namespace, name, provider)
......@@ -31,12 +43,18 @@ class ApiTerraregModuleVersionDetails(ErrorCatchingResource):
if version is not None:
module_version = terrareg.models.ModuleVersion.get(module_provider=module_provider, version=version)
else:
# If version has not been specified, attempt to get latest version
module_version = module_provider.get_latest_version()
# Otherwise, show provider provider details
if module_version is None:
return module_provider.get_terrareg_api_details()
if module_version is None:
return self._get_404_response()
return module_version.get_terrareg_api_details(
request_domain=urllib.parse.urlparse(request.base_url).hostname,
target_terraform_version=args.target_terraform_version
target_terraform_version=args.target_terraform_version,
html=(args.output == "html")
)
from flask_restful import reqparse
from terrareg.server.error_catching_resource import ErrorCatchingResource
import terrareg.auth_wrapper
......@@ -8,10 +10,26 @@ class ApiTerraregModuleVersionVariableTemplate(ErrorCatchingResource):
method_decorators = [terrareg.auth_wrapper.auth_wrapper('can_access_read_api')]
def _get_arg_parser(self):
"""Return argument parser for GET method"""
parser = reqparse.RequestParser()
parser.add_argument(
'output',
type=str,
location='args',
default='md',
dest='output',
help='Variable/Output description format, either "html" or "md"'
)
return parser
def _get(self, namespace, name, provider, version):
"""Return variable template."""
parser = self._get_arg_parser()
args = parser.parse_args()
_, _, _, module_version, error = self.get_module_version_by_name(
namespace, name, provider, version)
if error:
return error
return module_version.variable_template
\ No newline at end of file
return module_version.get_variable_template(html=(args.output == "html"))
......@@ -69,16 +69,13 @@ class ErrorCatchingResource(Resource, BaseHandler):
try:
return self._delete(*args, **kwargs)
except terrareg.errors.TerraregError as exc:
return {
"status": "Error",
"message": str(exc)
}, 500
return api_error(str(exc)), 500
def _get_404_response(self):
def _get_404_response(self) -> Tuple[Dict[str, str], int]:
"""Return common 404 error"""
return {'errors': ['Not Found']}, 404
def _get_401_response(self):
def _get_401_response(self) -> Tuple[Dict[str, str], int]:
"""Return standardised 401."""
return {'message': ('The server could not verify that you are authorized to access the URL requested. '
'You either supplied the wrong credentials (e.g. a bad password), '
......
......@@ -32,4 +32,17 @@
}
#settings-module-version-card {
margin-bottom: 15px;
}
#module-tab-inputs tr {
border: 1px #eeeeee solid;
}
#module-tab-inputs th {
padding: 10px 5px 10px 5px;
}
#module-tab-inputs td {
border-left: 1px #eeeeee solid;
padding: 10px 5px 10px 5px;
}
#module-tab-inputs tr:nth-child(even) {
background-color: #fcfcfc;
}
\ No newline at end of file
......@@ -219,11 +219,11 @@ terraregModuleDetailsPromiseSingleton = [];
async function getModuleDetails(module_id) {
// Create promise if it hasn't already been defined
if (terraregModuleDetailsPromiseSingleton[module_id] === undefined) {
let userPreferences = await getUserPreferences();
terraregModuleDetailsPromiseSingleton[module_id] = new Promise((resolve, reject) => {
let userPreferences = getUserPreferences();
let terraformVersionConstraintQueryString = (
userPreferences['terraform-compatibility-version'] ?
`?target_terraform_version=${userPreferences['terraform-compatibility-version']}` :
`?target_terraform_version=${userPreferences['terraform-compatibility-version']}&output=html` :
''
);
......@@ -251,7 +251,7 @@ function getUsageBuilderVariables(moduleId) {
// Perform request to obtain usage builder variables details
$.ajax({
type: "GET",
url: `/v1/terrareg/modules/${moduleId}/variable_template`,
url: `/v1/terrareg/modules/${moduleId}/variable_template?output=html`,
success: function (data) {
resolve(data);
},
......
......@@ -13,9 +13,12 @@ class TabFactory {
this._tabsLookup[tab.name] = tab;
this._tabs.push(tab);
}
getTabByName(tab) {
return this._tabsLookup[tab]
}
async renderTabs() {
for (const tab of this._tabs) {
tab.render();
tab.render(this);
}
for (const tab of this._tabs) {
await tab._renderPromise;
......@@ -72,7 +75,7 @@ class BaseTab {
constructor() {
this._renderPromise = undefined;
}
render() { }
render(tabFactory) { }
async isValid() {
let result = await this._renderPromise;
return result;
......@@ -84,7 +87,7 @@ class ModuleDetailsTab extends BaseTab {
super();
this._moduleDetails = moduleDetails;
}
render() { }
render(tabFactory) { }
}
......@@ -96,7 +99,7 @@ class ReadmeTab extends BaseTab {
get name() {
return 'readme';
}
render() {
render(tabFactory) {
this._renderPromise = new Promise((resolve) => {
if (!this._readmeUrl) {
resolve(false);
......@@ -136,7 +139,7 @@ class AdditionalTab extends BaseTab {
get name() {
return this._name;
}
render() {
render(tabFactory) {
this._renderPromise = new Promise((resolve) => {
if (!this._fileUrl) {
resolve(false);
......@@ -148,7 +151,7 @@ class AdditionalTab extends BaseTab {
return;
}
let tab = $(`<div id="module-tab-${this.name}" class="module-tabs content default-hidden"></div>`);
let tab = $(`<div id="module-tab-${this.name}" class="module-tabs content default-hidden column is-three-fifths is-offset-one-fifth"></div>`);
// Populate file conrtent
tab.append(fileContent);
......@@ -182,7 +185,7 @@ class AnalyticsTab extends ModuleDetailsTab {
get name() {
return 'analytics';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
// Check if analytics are disabled
......@@ -230,7 +233,7 @@ class ExampleFilesTab extends ModuleDetailsTab {
get name() {
return 'example-files';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
$.get(`/v1/terrareg/modules/${this._moduleDetails.id}/examples/filelist/${this._exampleDetails.path}`, (data) => {
if (!data.length) {
......@@ -270,34 +273,143 @@ class InputsTab extends ModuleDetailsTab {
get name() {
return 'inputs';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let inputTab = $("#module-tab-inputs");
let inputTabTbody = inputTab.find("tbody");
this._moduleDetails.inputs.forEach((input) => {
let inputRow = $("<tr></tr>");
let nameTd = $("<td></td>");
nameTd.text(input.name);
inputRow.append(nameTd);
let descriptionTd = $("<td></td>");
descriptionTd.text(input.description);
// Replace new line characters in description with line breaks
descriptionTd.html(descriptionTd.html().replace(/\n/g, '<br />'));
inputRow.append(descriptionTd);
// Clear out existing content after re-rendering
let inputLeft = $("#module-tab-inputs-left");
let inputContent = $("#module-tab-inputs-content");
inputLeft.html("");
inputContent.html("");
let userPreferences = await getUserPreferences();
inputLeft.append(getInputOutputViewSelect(userPreferences["ui-details-view"], (ev) => {
setUiDetailsView(ev.target.value);
// Re-render this page and outputs tab
this.render(tabFactory);
let outputTab = tabFactory.getTabByName("outputs")
if (outputTab !== undefined) {
outputTab.render(tabFactory);
}
}));
inputLeft.append($("<hr />"));
if (userPreferences["ui-details-view"] === "expanded") {
let requiredInputTab = $("<div id=\"module-tab-inputs-required\"></div>");
let optionalInputTab = $("<div id=\"module-tab-inputs-optional\"></div>");
let requiredToc = $("<div id=\"module-tab-inputs-toc-required\"</div>");
let optionalToc = $("<div id=\"module-tab-inputs-toc-optional\"></div>");
const replaceStartWhitespaceRe = new RegExp('^ ', 'mg');
this._moduleDetails.inputs.forEach((input) => {
let inputRow = $(`<div></div>`);
let anchorName = `terrareg-anchor-input-${input.name.replace(/[^A-Aa-z]/g, '')}`;
inputRow.attr('id', anchorName);
inputRow.attr('name', anchorName);
let nameTd = $("<h4 class='subtitle is-4'></h4>");
nameTd.text(input.name);
inputRow.append(nameTd);
let typeTd = $("<p></p>");
typeTd.text(`Type: `);
// If type contains any new line characters, use a pre-formatted block.
// Otherwise, use an inline code block
if (input.type.indexOf("\n") !== -1) {
let typeValue = $("<pre></pre>");
typeValue.text(input.type.replaceAll(replaceStartWhitespaceRe, ""));
typeTd.append(typeValue);
} else {
let typeValue = $("<code></code>");
typeValue.text(input.type);
typeTd.append(typeValue);
}
inputRow.append(typeTd);
let defaultTd = $("<p></p>");
defaultTd.text(input.required ? "This variable is required" : "Default: ");
let defaultValue = JSON.stringify(input.default, null, 2);
if (input.required !== true) {
if (defaultValue.indexOf("\n") !== -1) {
let defaultValueEl = $("<pre></pre>");
defaultValueEl.text(defaultValue);
defaultTd.append(defaultValueEl);
} else {
let defaultValueEl = $("<code></code>");
defaultValueEl.text(defaultValue);
defaultTd.append(defaultValueEl);
}
}
inputRow.append(defaultTd);
inputRow.append("<br />");
if (input.description && input.description.trim() != "") {
let descriptionTd = $("<div><b>Description</b><br /></div>");
let descriptionContent = $("<p></p>");
descriptionContent.html(input.description);
descriptionTd.append(descriptionContent);
inputRow.append(descriptionTd);
inputRow.append("<br />");
}
let typeTd = $("<td></td>");
typeTd.text(input.type);
inputRow.append(typeTd);
let toc = optionalToc;
let inputTab = optionalInputTab;
if (input.required) {
toc = requiredToc;
inputTab = requiredInputTab;
}
let defaultTd = $("<td></td>");
defaultTd.text(input.required ? "Required" : JSON.stringify(input.default));
inputRow.append(defaultTd);
let tocLink = $("<a></a>");
tocLink.attr('href', `#${anchorName}`);
tocLink.text(input.name);
toc.append(tocLink);
toc.append(`${input.required ? ' (required)' : ''}<br />`);
inputTab.append(inputRow);
inputTab.append($("<hr />"));
});
inputTabTbody.append(inputRow);
});
inputLeft.append(requiredToc);
inputLeft.append(optionalToc);
inputContent.append(requiredInputTab);
inputContent.append(optionalInputTab);
} else {
let inputTab = $("#module-tab-inputs-content");
let inputTable = $(`<table class="table module-provider-tab-content-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Type</th>
<th>Default value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>`);
let inputTabTbody = inputTable.find("tbody");
this._moduleDetails.inputs.forEach((input) => {
let inputRow = $("<tr></tr>");
let nameTd = $("<td></td>");
nameTd.text(input.name);
inputRow.append(nameTd);
let descriptionTd = $("<td></td>");
descriptionTd.html(input.description);
inputRow.append(descriptionTd);
let typeTd = $("<td></td>");
typeTd.text(input.type);
inputRow.append(typeTd);
let defaultTd = $("<td></td>");
defaultTd.text(input.required ? "Required" : JSON.stringify(input.default));
inputRow.append(defaultTd);
inputTabTbody.append(inputRow);
});
inputTab.append(inputTable);
}
// Show tab link
$('#module-tab-link-inputs').removeClass('default-hidden');
......@@ -310,25 +422,84 @@ class OutputsTab extends ModuleDetailsTab {
get name() {
return 'outputs';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let outputTab = $("#module-tab-outputs");
let outputTabTbody = outputTab.find("tbody");
this._moduleDetails.outputs.forEach((output) => {
let outputRow = $("<tr></tr>");
let nameTd = $("<td></td>");
nameTd.text(output.name);
outputRow.append(nameTd);
// Clear out existing content after re-rendering
let outputLeft = $("#module-tab-outputs-left");
let outputContent = $("#module-tab-outputs-content");
outputLeft.html("");
outputContent.html("");
let userPreferences = await getUserPreferences();
outputLeft.append(getInputOutputViewSelect(userPreferences["ui-details-view"], (ev) => {
setUiDetailsView(ev.target.value);
// Re-render page
this.render(tabFactory);
let inputTab = tabFactory.getTabByName("inputs")
if (inputTab !== undefined) {
inputTab.render(tabFactory);
}
}));
outputLeft.append($("<hr />"));
if (userPreferences["ui-details-view"] === "expanded") {
this._moduleDetails.outputs.forEach((output) => {
let outputRow = $(`<div></div>`);
let anchorName = `terrareg-anchor-output-${output.name.replace(/[^A-Aa-z]/g, '')}`;
outputRow.attr('id', anchorName);
outputRow.attr('name', anchorName);
let nameTd = $("<h4 class='subtitle is-4'></h4>");
nameTd.text(output.name);
outputRow.append(nameTd);
if (output.description && output.description.trim() != "") {
let descriptionTd = $("<div><b>Description</b><br /></div>");
let descriptionContent = $("<p></p>");
descriptionContent.html(output.description);
descriptionTd.append(descriptionContent);
outputRow.append(descriptionTd);
outputRow.append("<br />");
}
let descriptionTd = $("<td></td>");
descriptionTd.text(output.description);
// Replace new line characters in description with line breaks
descriptionTd.html(descriptionTd.html().replace(/\n/g, '<br />'));
outputRow.append(descriptionTd);
let tocLink = $("<a></a>");
tocLink.attr('href', `#${anchorName}`);
tocLink.text(output.name);
outputLeft.append(tocLink);
outputLeft.append("<br />");
outputContent.append(outputRow);
outputContent.append($("<hr />"));
});
} else {
let outputTab = $("#module-tab-outputs-content");
let outputTable = $(`<table class="table module-provider-tab-content-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
</tbody>
</table>`);
let outputTabTbody = outputTable.find("tbody");
this._moduleDetails.outputs.forEach((output) => {
let outputRow = $("<tr></tr>");
let nameTd = $("<td></td>");
nameTd.text(output.name);
outputRow.append(nameTd);
let descriptionTd = $("<td></td>");
descriptionTd.html(output.description);
outputRow.append(descriptionTd);
outputTabTbody.append(outputRow);
});
outputTab.append(outputTable);
}
outputTabTbody.append(outputRow);
});
// Show tab link
$('#module-tab-link-outputs').removeClass('default-hidden');
......@@ -341,7 +512,7 @@ class ModulesTab extends ModuleDetailsTab {
get name() {
return 'modules';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let modulesTab = $("#module-tab-modules");
let modulesTabTbody = modulesTab.find("tbody");
......@@ -374,7 +545,7 @@ class ProvidersTab extends ModuleDetailsTab {
get name() {
return 'providers';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let providerTab = $("#module-tab-providers");
let providerTabTbody = providerTab.find("tbody");
......@@ -411,7 +582,7 @@ class SecurityIssuesTab extends ModuleDetailsTab {
get name() {
return 'security-issues';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
if (this._moduleDetails.security_results) {
let tfsecTab = $("#module-tab-security-issues");
......@@ -538,7 +709,7 @@ class ResourcesTab extends ModuleDetailsTab {
get name() {
return 'resources';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
// Populate link to resources graph
$('#resourceDependencyGraphLink').on("click", () => {window.location.href = this._graphUrl});
......@@ -590,7 +761,7 @@ class IntegrationsTab extends ModuleDetailsTab {
get name() {
return 'integrations';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let config = await getConfig();
let loggedIn = await isLoggedIn();
......@@ -666,7 +837,7 @@ class SettingsTab extends ModuleDetailsTab {
get name() {
return 'settings';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let userPermissions = await isLoggedIn();
......@@ -993,7 +1164,7 @@ class BaseUsageBuilderRow {
let additionalHelpTd = $('<td></td>');
additionalHelpTd.attr('style', 'width: 50%');
additionalHelpTd.text(this.config.additional_help ? this.config.additional_help : '');
additionalHelpTd.html(this.config.additional_help ? this.config.additional_help : '');
inputRow.append(additionalHelpTd);
let valueTd = $('<td></td>');
......@@ -1432,7 +1603,7 @@ class UsageBuilderTab extends ModuleDetailsTab {
get name() {
return 'usage-builder';
}
async render() {
async render(tabFactory) {
this._renderPromise = new Promise(async (resolve) => {
let usageBuilderTable = $('#usageBuilderTable');
......@@ -1555,6 +1726,19 @@ ${outputTf}
}
}
/*
*
*/
function getInputOutputViewSelect(currentValue, callback) {
let outerDiv = $("<div class=\"control select\"></div>");
let select = $("<select></select>");
select.append($(`<option value="table" ${currentValue == "table" ? "selected" : ""}>Table View</select>`));
select.append($(`<option value="expanded" ${currentValue == "expanded" ? "selected" : ""}>Expanded View</select>`));
select.on("change", callback);
outerDiv.append(select);
return outerDiv;
}
/*
* Setup router and call setup page depending on the page/module type
*/
......@@ -1687,13 +1871,13 @@ function populateVersionText(moduleDetails) {
*
* @param moduleDetails Terrareg module details
*/
function populateVersionSelect(moduleDetails) {
async function populateVersionSelect(moduleDetails) {
let versionSelection = $("#version-select");
let currentVersionFound = false;
let currentIsLatestVersion = false;
let userPreferences = getUserPreferences();
let userPreferences = await getUserPreferences();
$.get(`/v1/terrareg/modules/${moduleDetails.module_provider_id}/versions` +
`?include-beta=${userPreferences["show-beta-versions"]}&` +
......@@ -1995,7 +2179,7 @@ function onSubmoduleSelectChange(event) {
*/
async function populateTerraformUsageExample(moduleDetails, submoduleDetails) {
let config = await getConfig();
let userPreferences = getUserPreferences();
let userPreferences = await getUserPreferences();
// Check if module has been published - if not, do not
// show usage example
......
......@@ -12,21 +12,27 @@ function getLocalStorageValue(key, defaultValue, type) {
return value;
}
function getUserPreferences() {
async function getUserPreferences() {
let config = await getConfig();
return {
'show-beta-versions': getLocalStorageValue('show-beta-versions', false, Boolean),
'show-unpublished-versions': getLocalStorageValue('show-unpublished-versions', false, Boolean),
'theme': $.cookie("theme") || 'default',
'terraform-compatibility-version': getLocalStorageValue('terraform-compatibility-version', '', String),
'ui-details-view': getLocalStorageValue('ui-details-view', config.DEFAULT_UI_DETAILS_VIEW, String),
}
}
function setUiDetailsView(newValue) {
localStorage.setItem("ui-details-view", newValue);
}
function userPreferencesUpdateTerraformCompatibilityVersion(newVersion) {
localStorage.setItem('terraform-compatibility-version', newVersion);
}
function userPreferencesModalShow() {
let currentPreferences = getUserPreferences();
async function userPreferencesModalShow() {
let currentPreferences = await getUserPreferences();
$('#user-preferences-show-beta').prop('checked', currentPreferences["show-beta-versions"]);
$('#user-preferences-show-unpublished').prop('checked', currentPreferences["show-unpublished-versions"]);
$('#user-preferences-theme').val(currentPreferences["theme"]);
......
......@@ -252,15 +252,15 @@
</div>
<div class="columns">
<div class="column is-three-fifths is-offset-one-fifth">
<!-- <div class=""> -->
<!-- Tab content -->
<!-- README tab -->
<div id="module-tab-readme" class="module-tabs content default-hidden">
<div id="module-tab-readme" class="column is-three-fifths is-offset-one-fifth module-tabs content default-hidden">
</div>
<!-- Example files tab -->
<div id="module-tab-example-files" class="module-tabs default-hidden">
<div id="module-tab-example-files" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<div class="columns">
<div class="column is-one-fifths">
<nav id="example-file-list-nav" class="panel">
......@@ -277,37 +277,27 @@
</div>
<!-- Inputs tab -->
<div id="module-tab-inputs" class="module-tabs default-hidden">
<table class="table module-provider-tab-content-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Type</th>
<th>Default value</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="module-tab-inputs" class="module-tabs default-hidden column is-four-fifths">
<div class="columns">
<div id="module-tab-inputs-left" class="column is-one-third">
</div>
<div id="module-tab-inputs-content" class="column is-two-thirds">
</div>
</div>
</div>
<!-- Outputs tab -->
<div id="module-tab-outputs" class="module-tabs default-hidden">
<table class="table module-provider-tab-content-table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="module-tab-outputs" class="module-tabs default-hidden column is-four-fifths">
<div class="columns">
<div id="module-tab-outputs-left" class="column is-one-third">
</div>
<div id="module-tab-outputs-content" class="column is-two-thirds">
</div>
</div>
</div>
<!-- Providers tab -->
<div id="module-tab-providers" class="module-tabs default-hidden">
<div id="module-tab-providers" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<table class="table module-provider-tab-content-table">
<thead>
<tr>
......@@ -323,7 +313,7 @@
</div>
<!-- Resources tab -->
<div id="module-tab-resources" class="module-tabs default-hidden">
<div id="module-tab-resources" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<button class="button is-info is-small" id="resourceDependencyGraphLink">View resource dependency graph</button>
......@@ -345,7 +335,7 @@
</div>
<!-- Modules tab -->
<div id="module-tab-modules" class="module-tabs default-hidden">
<div id="module-tab-modules" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<table class="table module-modules-tab-content-table">
<thead>
......@@ -361,7 +351,7 @@
</div>
<!-- Security Issues tab -->
<div id="module-tab-security-issues" class="module-tabs default-hidden">
<div id="module-tab-security-issues" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<div class="table-container">
<table id="security-issues-table" class="table display compact nowrap responsive" style="width:100%">
<thead>
......@@ -388,7 +378,7 @@
</div>
<!-- Analytics tab -->
<div id="module-tab-analytics" class="module-tabs default-hidden">
<div id="module-tab-analytics" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<table id="analytics-table" class="table module-provider-tab-content-table">
<thead>
<tr>
......@@ -404,7 +394,7 @@
</div>
<!-- Usage Builder tab -->
<div id="module-tab-usage-builder" class="module-tabs default-hidden">
<div id="module-tab-usage-builder" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<div class="notification has-text-centered">Provide values to the following inputs to generate Terraform required to use the module.</div><br />
<form name="frm-example" id="frm-example">
<table id="usage-builder-table" class="table is-flex-wrap-wrap module-provider-tab-content-table" style="width: 100%;">
......@@ -425,7 +415,7 @@
</div>
<!-- Integrations tab -->
<div id="module-tab-integrations" class="module-tabs default-hidden">
<div id="module-tab-integrations" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<p class="subtitle">
Integrations
</p>
......@@ -459,7 +449,7 @@
</div>
<!-- Module settings tab -->
<div id="module-tab-settings" class="module-tabs default-hidden">
<div id="module-tab-settings" class="column is-three-fifths is-offset-one-fifth module-tabs default-hidden">
<div id="settings-module-version-card" class="card default-hidden">
<div class="card-content" style="background: #edf7ff">
<h2 class="subtitle is-h2">Module Version</h2>
......@@ -682,7 +672,7 @@
</div>
</div>
</div>
<!-- </div> -->
</div>
......
......@@ -189,8 +189,8 @@
}
}
function loadSearchUserPreferences() {
let userPreferences = getUserPreferences();
async function loadSearchUserPreferences() {
let userPreferences = await getUserPreferences();
$('#search-terraform-version').val(userPreferences['terraform-compatibility-version']);
}
......@@ -202,12 +202,12 @@
performSearch(currentSearchMeta?.current_offset);
}
$(document).ready(function () {
$(document).ready(async function () {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const searchQuery = urlParams.get('q');
loadSearchUserPreferences();
await loadSearchUserPreferences();
if (searchQuery) {
$('#search-query-string').val(searchQuery);
......
......@@ -183,19 +183,23 @@ class TerraformIdp:
config = terrareg.config.Config()
if config.ALLOW_UNAUTHENTICATED_ACCESS:
print("Disabling Terraform OIDC provider due to ALLOW_UNAUTHENTICATED_ACCESS is true")
if config.DEBUG:
print("Disabling Terraform OIDC provider due to ALLOW_UNAUTHENTICATED_ACCESS is true")
return False
if not config.TERRAFORM_OIDC_IDP_SUBJECT_ID_HASH_SALT:
print('Disabling Terraform OIDC provider due to missing TERRAFORM_OIDC_IDP_SUBJECT_ID_HASH_SALT')
if config.DEBUG:
print('Disabling Terraform OIDC provider due to missing TERRAFORM_OIDC_IDP_SUBJECT_ID_HASH_SALT')
return False
if not os.path.isfile(config.TERRAFORM_OIDC_IDP_SIGNING_KEY_PATH):
print('Disabling Terraform OIDC provider due to TERRAFORM_OIDC_IDP_SIGNING_KEY_PATH not set to a present file')
if config.DEBUG:
print('Disabling Terraform OIDC provider due to TERRAFORM_OIDC_IDP_SIGNING_KEY_PATH not set to a present file')
return False
if not config.PUBLIC_URL:
print('Disabling Terraform OIDC provider due to missing PUBLIC_URL config')
if config.DEBUG:
print('Disabling Terraform OIDC provider due to missing PUBLIC_URL config')
return False
return True
......
......@@ -575,7 +575,17 @@ class TestModuleVersion(TerraregIntegrationTest):
# Ensure when the auto-generated usage builder is disabled, only the pre-defined
# variables in the module version are returned
with unittest.mock.patch('terrareg.config.Config.AUTOGENERATE_USAGE_BUILDER_VARIABLES', False):
assert module_version.variable_template == [
assert module_version.get_variable_template() == [
{
'additional_help': 'Provide the name of the application',
'name': 'name_of_application',
'quote_value': True,
'type': 'text',
'default_value': None,
'required': True
}
]
assert module_version.get_variable_template(html=True) == [
{
'additional_help': 'Provide the name of the application',
'name': 'name_of_application',
......@@ -589,7 +599,7 @@ class TestModuleVersion(TerraregIntegrationTest):
# Ensure when auto-generated usage builder is enabled, the missing required variables
# are populated in the variable template
with unittest.mock.patch('terrareg.config.Config.AUTOGENERATE_USAGE_BUILDER_VARIABLES', True):
assert module_version.variable_template == [
assert module_version.get_variable_template() == [
{
'additional_help': 'Provide the name of the application',
'name': 'name_of_application',
......@@ -599,7 +609,7 @@ class TestModuleVersion(TerraregIntegrationTest):
'required': True
},
{
'additional_help': 'Override the default string',
'additional_help': 'Override the *default* string',
'default_value': 'this is the default',
'name': 'string_with_default_value',
'quote_value': True,
......@@ -608,7 +618,7 @@ class TestModuleVersion(TerraregIntegrationTest):
},
{
'additional_help': 'Override the default string',
'default_value': None,
'default_value': 'Default with _markdown_!',
'name': 'undocumented_required_variable',
'quote_value': True,
'required': True,
......@@ -632,8 +642,60 @@ class TestModuleVersion(TerraregIntegrationTest):
},
{
'additional_help': 'Override the stringy list',
'default_value': ['value 1',
'value 2'],
'default_value': ['value 1', 'value 2'],
'name': 'example_list_input',
'quote_value': True,
'required': False,
'type': 'text'
}
]
# Test variable template with HTML
assert module_version.get_variable_template(html=True) == [
{
# Values obtained from terrareg.json should converted to HTML
'additional_help': 'Provide the name of the application',
'name': 'name_of_application',
'quote_value': True,
'type': 'text',
'default_value': None,
'required': True
},
{
'additional_help': '<p>Override the <em>default</em> string</p>',
'default_value': 'this is the default',
'name': 'string_with_default_value',
'quote_value': True,
'required': False,
'type': 'text'
},
{
'additional_help': '<p>Override the default string</p>',
'default_value': 'Default with _markdown_!',
'name': 'undocumented_required_variable',
'quote_value': True,
'required': True,
'type': 'text'
},
{
'additional_help': '<p>required boolean variable</p>',
'default_value': None,
'name': 'example_boolean_input',
'quote_value': False,
'required': True,
'type': 'boolean'
},
{
'additional_help': '<p>A required list</p>',
'default_value': None,
'name': 'required_list_variable',
'quote_value': True,
'required': True,
'type': 'list'
},
{
'additional_help': '<p>Override the stringy list</p>',
'default_value': ['value 1', 'value 2'],
'name': 'example_list_input',
'quote_value': True,
'required': False,
......