Alright, now that we've verified that we have a working back-end of our plugin, and have a local deployment where we can browse to our UI, it's time to make the front-end work, and look a bit nicer.
We're still going to keep our front-end pretty simple, so all we're going to do is have our landing page list our issues, allow a user to create a new issue, and be able to update an issue's status, or delete an issue. To allow for this, we'll need a client we can use to do all these things.
In the future, the project add frontend
will auto-generate this boilerplate client, but for now, we have to write it ourselves.
For this tutorial, we have one pre-written, that we'll discuss a few parts of. Either create a new file called plugin/src/api/issue_client.ts
and copy the contents of this file into it, or run the following to do it automatically:
mkdir -p plugin/src/api && curl -o plugin/src/api/issue_client.ts https://raw.githubusercontent.com/grafana/grafana-app-sdk/main/docs/tutorials/issue-tracker/frontend-files/issue-client.ts
The client uses grafana libraries to make fetch requests to perform relevent actions, and uses the generated Issue
type in generated/issue/v1/issue_object_gen.ts
that mirrors our generated go v1.Issue
type. We have methods for get
, list
, create
, update
, and delete
. We'll use these methods in our update to the main page of the plugin.
We already have a very empty generated main page located at plugin/src/pages/main.tsx
. We're going to overwrite all of this with new contents.
Either copy the contents of this file into plugin/src/pages/PageOne.tsx
(overwriting the current contents), or do it with curl:
curl -o plugin/src/pages/PageOne.tsx https://raw.githubusercontent.com/grafana/grafana-app-sdk/main/docs/tutorials/issue-tracker/frontend-files/main.tsx
We've now updated the main page to display a list of our Issue
resources, with some basic options.
const [issuesData, setIssuesData] = useState(issues);
useEffect(() => {
const fetchData = async () => {
const client = new IssueClient()
const issues = await client.list();
setIssuesData(issues.data.items);
}
fetchData().catch(console.error);
}, []);
Here we list issues using the IssueClient
we created in the previous section, and push them into the state, so that our issue list returned by this function is populated with the initial list of issues from the API server.
Next we define a few functions to call to manipulate issues:
// IssueClient to share for all our functions
const ic = new IssueClient();
const listIssues = async() => {
const issues = await ic.list();
setIssuesData(issues.data.items);
}
const createIssue = async (title: string, description: string) => {
await ic.create(title, description);
await listIssues();
};
const deleteIssue = async (id: string) => {
await ic.delete(id);
await listIssues();
};
const updateStatus = async (issue: Issue, newStatus: string) => {
issue.spec.status = newStatus;
await ic.update(issue.metadata.name, issue);
await listIssues();
}
Finally, we have to display the information. We define one more function:
const getActions = (issue: Issue) => {
if(issue.spec.status === 'open') {
return (
<Card.Actions>
<Button key="mark-in-progress" onClick={() => {updateStatus(issue, 'in_progress')}}>Start Progress</Button>
</Card.Actions>
)
} else if(issue.spec.status === 'in_progress') {
return (
<Card.Actions>
<Button key="mark-open" onClick={() => {updateStatus(issue, 'open')}}>Stop Progress</Button>
<Button key="mark-closed" onClick={() => {updateStatus(issue, 'closed')}}>Complete</Button>
</Card.Actions>
)
} else {
return <Card.Actions></Card.Actions>
}
}
That we'll use in our display output to show the correct Issue status and display button(s) to transition its status. Then it's just a matter of returning the appropriate HTML in our return
function, populating the data from issuesData
where appropriate, which we know will get set to the list of issues from the API server.
Now we want to redeploy our plugin front-end to see the changes. Since we don't need to rebuild the operator or the plugin's backend, we can just do
$ make build/plugin-frontend
After that completes, we can redploy to our active local environment with
$ make local/deploy_plugin
And just like that, we can refresh or go to [http://grafana.k3d.localhost:9999/a/issuetrackerproject-app/], and see our brand-new plugin UI.
If we create a new issue, we can see that it shows up in the list, or via a kubectl get issues
.
Now all that's left is to think a bit about our operator.