Skip to content

Commit

Permalink
Handle retired and deprecated tools in the frontend
Browse files Browse the repository at this point in the history
- If a tool is depcrecated, show the deprecation message when it is selected
- If a tool is retired, hide it and show a message that user must upgrade
  • Loading branch information
michaeljcollinsuk committed Dec 9, 2024
1 parent c64cdf6 commit 9ad1f2e
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 25 deletions.
21 changes: 21 additions & 0 deletions controlpanel/api/migrations/0050_alter_tool_deprecated_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 5.1.2 on 2024-12-09 15:07

# Third-party
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0049_tool_deprecated_message_tool_is_retired"),
]

operations = [
migrations.AlterField(
model_name="tool",
name="deprecated_message",
field=models.TextField(
blank=True, help_text="If no message is provided, a default message will be used."
),
),
]
15 changes: 14 additions & 1 deletion controlpanel/api/models/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Tool(TimeStampedModel):
"rstudio": "rstudio",
"vscode": "vscode",
}
DEFAULT_DEPRECATED_MESSAGE = "The selected release has been deprecated and will be retired soon. Please update to a more recent version." # noqa

description = models.TextField(blank=True)
chart_name = models.CharField(max_length=100, blank=False)
Expand All @@ -52,7 +53,9 @@ class Tool(TimeStampedModel):
)

is_deprecated = models.BooleanField(default=False)
deprecated_message = models.TextField(blank=True)
deprecated_message = models.TextField(
blank=True, help_text="If no message is provided, a default message will be used."
)
is_retired = models.BooleanField(default=False)

class Meta(TimeStampedModel.Meta):
Expand Down Expand Up @@ -83,6 +86,16 @@ def image_tag(self):
"{}.image.tag".format(chart_image_key_name)
)

@property
def get_deprecated_message(self):
if not self.is_deprecated:
return ""

if self.is_retired:
return ""

return self.deprecated_message or self.DEFAULT_DEPRECATED_MESSAGE


class ToolDeploymentManager:
"""
Expand Down
6 changes: 3 additions & 3 deletions controlpanel/frontend/jinja2/release-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
},
"classes": "govuk-!-width-two-thirds",
"hint": {
"text": 'A flag to indicate the release is deprecated and will soon become unavailable to deploy'
"text": 'Checking this will display a deprecation message to users when they select this release'
},
"name": "is_deprecated",
"items": [
Expand All @@ -172,7 +172,7 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
},
"classes": "govuk-!-width-two-thirds",
"hint": {
"text": 'A message to display to users if they select this tool'
"text": form.deprecated_message.help_text
},
"name": "deprecated_message",
"value": form.deprecated_message.value(),
Expand All @@ -188,7 +188,7 @@ <h1 class="govuk-heading-xl">{{ page_title }}</h1>
},
"classes": "govuk-!-width-two-thirds",
"hint": {
"text": 'A flag to indicate the release is retired. Users will not be able to deploy this release.'
"text": 'Checking this will remove this release from all users dropdown options on the Your Tools page.'
},
"name": "is_retired",
"items": [
Expand Down
16 changes: 12 additions & 4 deletions controlpanel/frontend/jinja2/tool-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,22 @@ <h2 class="govuk-heading-m">{{ tool_info.name }}</h2>
{% if deployment and deployment.tool_id > -1 %}
{% set installed_chart_version = deployment.description %}
{% set installed_release_version = deployment.tool_id %}
<option class="installed" value="{{ deployment.chart_name }}__{{ installed_chart_version }}__{{ deployment.tool_id }}">
[{{ deployment.chart_name }} {{ deployment.image_tag }}] {{ installed_chart_version or "Unknown" }} (installed)
<option class="installed" value="{{ deployment.chart_name }}__{{ installed_chart_version }}__{{ deployment.tool_id }}"
data-is-deprecated="{{ deployment.is_deprecated }}"
data-deprecated-message="{{ deployment.deprecated_message }}">
{% if deployment.is_deprecated %}DEPRECATED {% endif %}[{{ deployment.chart_name }} {{ deployment.image_tag }}] {{ installed_chart_version or "Unknown" }} (installed)
</option>
{% else %}
<option class="not-installed">Not deployed - select a tool from this list and click "Deploy" to start</option>
{% endif %}
{% for release_version, release_detail in tool_info["releases"].items(): %}
{% if release_version != installed_release_version: %}
<option value="{{ release_detail.chart_name }}__{{ release_detail.chart_version }}__{{ release_detail.tool_id }}">
[{{ release_detail.chart_name }} {{ release_detail.image_tag }}] {{ release_detail.description or "Unknown" }}
<option
value="{{ release_detail.chart_name }}__{{ release_detail.chart_version }}__{{ release_detail.tool_id }}"
data-is-deprecated="{{ release_detail.is_deprecated }}"
data-deprecated-message="{{ release_detail.deprecated_message }}">
<!-- TODO rather than build this info up manually in the view we could read it from the tool object itself -->
{% if release_detail.is_deprecated %}DEPRECATED {% endif %}[{{ release_detail.chart_name }} {{ release_detail.image_tag }}] {{ release_detail.description or "Unknown" }}
</option>
{% endif %}
{% endfor %}
Expand Down Expand Up @@ -107,6 +113,8 @@ <h2 class="govuk-heading-m">{{ tool_info.name }}</h2>
</div>
</div>

<div id="{{ chart_name }}-deprecation-message" {% if not deployment or deployment.deprecated_message == "" %}class="govuk-visually-hidden"{% endif %}><p class="govuk-body">{{ deployment.deprecated_message }}</p></div>

{% if deployment and deployment.tool_id == -1 %}

<div>
Expand Down
16 changes: 16 additions & 0 deletions controlpanel/frontend/static/javascripts/modules/tool-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,21 @@ moj.Modules.toolStatus = {
// If "(not installed)" or "(installed)" version selected
// the "Deploy" button needs to be disabled
deployButton.disabled = notInstalledSelected || installedSelected;

this.toggleDeprecationMessage(selected, targetTool);
},

toggleDeprecationMessage(selected, targetTool) {
const isDeprecated = selected.attributes["data-is-deprecated"].value === "True";
const deprecationMessageElement = document.getElementById(targetTool.value + "-deprecation-message");
const deprecationMessage = selected.attributes["data-deprecated-message"].value;

if (isDeprecated) {
deprecationMessageElement.firstChild.innerText = deprecationMessage;
deprecationMessageElement.classList.remove(this.hidden);
} else {
deprecationMessageElement.classList.add(this.hidden);
deprecationMessageElement.firstChild.innerText = "";
}
}
};
46 changes: 29 additions & 17 deletions controlpanel/frontend/views/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,30 +71,35 @@ def _find_related_tool_record(self, chart_name, chart_version, image_tag):
"""
tool_set = Tool.objects.filter(
chart_name=chart_name, version=chart_version, is_restricted=False
)
for item in tool_set:
if item.image_tag == image_tag:
return item
return tool_set.first()
).exclude(is_retired=True)
for tool in tool_set:
if tool.image_tag == image_tag:
return tool
# If we cant find a tool with the same image tag, this must mean that it was retired or
# deleted. So return none, and let the calling function handle it
return None

def _add_new_item_to_tool_box(self, user, tool_box, tool, tools_info, charts_info):
def _add_new_item_to_tool_box(self, user, tool_box, tool, tools_info):
if tool_box not in tools_info:
tools_info[tool_box] = {
"name": tool.name,
"url": tool.url(user),
"deployment": None,
"releases": {},
}
image_tag = tool.image_tag
if not image_tag:
image_tag = charts_info.get(tool.version, {}) or "unknown"
# TODO We should update model to always store an image tag
# image_tag = tool.image_tag
# if not image_tag:
# image_tag = charts_info.get(tool.version, {}) or "unknown"
if tool.id not in tools_info[tool_box]["releases"]:
tools_info[tool_box]["releases"][tool.id] = {
"tool_id": tool.id,
"chart_name": tool.chart_name,
"description": tool.description,
"chart_version": tool.version,
"image_tag": image_tag,
"image_tag": tool.image_tag,
"is_deprecated": tool.is_deprecated,
"deprecated_message": tool.get_deprecated_message,
}

def _get_tool_deployed_image_tag(self, containers):
Expand All @@ -103,8 +108,10 @@ def _get_tool_deployed_image_tag(self, containers):
return container.image.split(":")[1]
return None

def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info):
def _add_deployed_charts_info(self, tools_info, user, id_token):
# Get list of deployed tools
# TODO this sets what tool the user currently has deployed. If we were to refactor to store
# deployed tools in the database, we could remove a lot of this logic
deployments = cluster.ToolDeployment.get_deployments(user, id_token)
for deployment in deployments:
chart_name, chart_version = deployment.metadata.labels["chart"].rsplit("-", 1)
Expand All @@ -119,7 +126,7 @@ def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info):
)
)
else:
self._add_new_item_to_tool_box(user, tool_box, tool, tools_info, charts_info)
self._add_new_item_to_tool_box(user, tool_box, tool, tools_info)
if tool_box not in tools_info:
# up to this stage, if the tool_box is still empty, it means
# there is no tool release available in db
Expand All @@ -131,21 +138,26 @@ def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info):
"image_tag": image_tag,
"description": tool.description if tool else "Not available",
"status": ToolDeployment(tool, user).get_status(id_token, deployment=deployment),
"is_deprecated": tool.is_deprecated if tool else False,
"deprecated_message": tool.get_deprecated_message if tool else "",
"is_retired": tool is None,
}

def _retrieve_detail_tool_info(self, user, tools, charts_info):
def _retrieve_detail_tool_info(self, user, tools):
# TODO why do we need this? We could change so that all information required about available tools comes from the DB # noqa: E501
tools_info = {}
for tool in tools:
# Work out which bucket the chart should be in
tool_box = self._locate_tool_box_by_chart_name(tool.chart_name)
# No matching tool bucket for the given chart. So ignore.
if tool_box:
self._add_new_item_to_tool_box(user, tool_box, tool, tools_info, charts_info)
self._add_new_item_to_tool_box(user, tool_box, tool, tools_info)
return tools_info

def _get_charts_info(self, tool_list):
# We may need the default image_tag from helm chart
# unless we configure it specifically in parameters of tool release
# TODO if we make sure that we always have an image_tag for a tool, then building charts_info is redundant and could be removed # noqa: E501
charts_info = {}
chart_entries = None
for tool in tool_list:
Expand Down Expand Up @@ -229,14 +241,14 @@ def get_context_data(self, *args, **kwargs):
context["managed_airflow_prod_url"] = f"{settings.AWS_SERVICE_URL}/?{args_airflow_prod_url}"

# Arrange tools information
charts_info = self._get_charts_info(context["tools"])
tools_info = self._retrieve_detail_tool_info(user, context["tools"], charts_info)
# charts_info = self._get_charts_info(context["tools"])
tools_info = self._retrieve_detail_tool_info(user, context["tools"])

if "vscode" in tools_info:
url = tools_info["vscode"]["url"]
tools_info["vscode"]["url"] = f"{url}?folder=/home/analyticalplatform/workspace"

self._add_deployed_charts_info(tools_info, user, id_token, charts_info)
self._add_deployed_charts_info(tools_info, user, id_token)
context["tools_info"] = tools_info
return context

Expand Down

0 comments on commit 9ad1f2e

Please sign in to comment.