Skip to content

Commit 90b3e85

Browse files
authored
feat(ui): support custom icons (#20864)
Signed-off-by: Michael Crenshaw <[email protected]>
1 parent 1b973b8 commit 90b3e85

File tree

29 files changed

+244
-12
lines changed

29 files changed

+244
-12
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,12 @@ clidocsgen:
261261
actionsdocsgen:
262262
hack/generate-actions-list.sh
263263

264+
.PHONY: resourceiconsgen
265+
resourceiconsgen:
266+
hack/generate-icons-typescript.sh
267+
264268
.PHONY: codegen-local
265-
codegen-local: mod-vendor-local mockgen gogen protogen clientgen openapigen clidocsgen actionsdocsgen manifests-local notification-docs notification-catalog
269+
codegen-local: mod-vendor-local mockgen gogen protogen clientgen openapigen clidocsgen actionsdocsgen resourceiconsgen manifests-local notification-docs notification-catalog
266270
rm -rf vendor/
267271

268272
.PHONY: codegen-local-fast
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
The Argo CD UI displays icons for various Kubernetes resource types to help users quickly identify them. Argo CD
2+
includes a set of built-in icons for common resource types.
3+
4+
You can contribute additional icons for custom resource types by following these steps:
5+
6+
1. Ensure the license is compatible with Apache 2.0.
7+
2. Add the icon file to the `ui/src/assets/images/resources/<group>/icon.svg` path in the Argo CD repository.
8+
3. Modify the SVG to use the correct color, `#8fa4b1`.
9+
4. Run `make resourceiconsgen` to update the generated typescript file that lists all available icons.
10+
5. Create a pull request to the Argo CD repository with your changes.
11+
12+
`<group>` is the API group of the custom resource. For example, if you are adding an icon for a custom resource with the
13+
API group `example.com`, you would place the icon at `ui/src/assets/images/resources/example.com/icon.svg`.
14+
15+
If you want the same icon to apply to resources in multiple API groups with the same suffix, you can create a directory
16+
prefixed with an underscore. The underscore will be interpreted as a wildcard. For example, to apply the same icon to
17+
resources in the `example.com` and `another.example.com` API groups, you would place the icon at
18+
`ui/src/assets/images/resources/_.example.com/icon.svg`.

hack/generate-icons-typescript.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
# Users may configure custom resource icons in resource_customizations. This script generates a list of those icons so
4+
# that the UI knows which icons are available.
5+
6+
{
7+
echo "// Code generated by hack/generate-icons-typescript.sh; DO NOT EDIT.";
8+
echo "/* eslint-disable prettier/prettier */";
9+
echo "";
10+
echo "// resourceIconGroups is a map of resource kind globs to whether or not a custom icon exists for that kind.";
11+
echo "// Each glob corresponds to a directory under ui/src/assets/images/resources, where any asterisk is represented as an underscore (_).";
12+
echo "export const resourceIconGroups = {";
13+
find ui/src/assets/images/resources -name icon.svg | sort | sed "s/ui\/src\/assets\/images\/resources\// '/" | sed "s/\/icon.svg/': true,/" | sed 's/_/*/';
14+
echo "};";
15+
} > ui/src/app/applications/components/resource-customizations.ts

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ nav:
229229
- developer-guide/extensions/proxy-extensions.md
230230
- developer-guide/faq.md
231231
- developer-guide/tilt.md
232+
- developer-guide/custom-resource-icons.md
232233
- faq.md
233234
- security_considerations.md
234235
- Support: SUPPORT.md

ui/src/app/applications/components/application-details/application-resource-list.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export const ApplicationResourceList = (props: ApplicationResourceListProps) =>
175175
<div className='row'>
176176
<div className='columns small-1 xxxlarge-1'>
177177
<div className='application-details__resource-icon'>
178-
<ResourceIcon kind={res.kind} />
178+
<ResourceIcon group={res.group} kind={res.kind} />
179179
<br />
180180
<div>{ResourceLabel({kind: res.kind})}</div>
181181
</div>

ui/src/app/applications/components/application-pod-view/pod-view.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export function PodView(props: PodViewProps) {
126126
style={group.kind === 'node' ? {} : {cursor: 'pointer'}}>
127127
<div style={{display: 'flex', alignItems: 'center'}}>
128128
<div style={{marginRight: '10px'}}>
129-
<ResourceIcon kind={group.kind || 'Unknown'} />
129+
<ResourceIcon group={group.group} kind={group.kind || 'Unknown'} />
130130
<br />
131131
{<div style={{textAlign: 'center'}}>{ResourceLabel({kind: group.kind})}</div>}
132132
</div>

ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: n
284284
<React.Fragment>
285285
<div className='application-resource-tree__node' style={{left: node.x, top: node.y, width: node.width, height: node.height}}>
286286
<div className='application-resource-tree__node-kind-icon'>
287-
<ResourceIcon kind={node.kind} />
287+
<ResourceIcon group={node.group} kind={node.kind} />
288288
<br />
289289
<div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>
290290
</div>
@@ -462,7 +462,7 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
462462
className={classNames('application-resource-tree__node-kind-icon', {
463463
'application-resource-tree__node-kind-icon--big': rootNode
464464
})}>
465-
<ResourceIcon kind={node.kind || 'Unknown'} />
465+
<ResourceIcon group={node.group} kind={node.kind || 'Unknown'} />
466466
<br />
467467
{!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
468468
</div>
@@ -746,7 +746,7 @@ function renderResourceNode(props: ApplicationResourceTreeProps, id: string, nod
746746
className={classNames('application-resource-tree__node-kind-icon', {
747747
'application-resource-tree__node-kind-icon--big': rootNode
748748
})}>
749-
<ResourceIcon kind={node.kind} />
749+
<ResourceIcon group={node.group} kind={node.kind} />
750750
<br />
751751
{!rootNode && <div className='application-resource-tree__node-kind'>{ResourceLabel({kind: node.kind})}</div>}
752752
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Code generated by hack/generate-icons-typescript.sh; DO NOT EDIT.
2+
/* eslint-disable prettier/prettier */
3+
4+
// resourceIconGroups is a map of resource kind globs to whether or not a custom icon exists for that kind.
5+
// Each glob corresponds to a directory under ui/src/assets/images/resources, where any asterisk is represented as an underscore (_).
6+
export const resourceIconGroups = {
7+
'*.crossplane.io': true,
8+
'*.fluxcd.io': true,
9+
'*.knative.dev': true,
10+
'cassandra.rook.io': true,
11+
'cert-manager.io': true,
12+
'core.spinkube.dev': true,
13+
'external-secrets.io': true,
14+
'flagger.app': true,
15+
'install.istio.io': true,
16+
'jaegertracing.io': true,
17+
'k8s.keycloak.org': true,
18+
'kafka.strimzi.io': true,
19+
'keda.sh': true,
20+
'kubevirt.io': true,
21+
'kyverno.io': true,
22+
'opentelemetry.io': true,
23+
'projectcontour.io': true,
24+
'work.karmada.io': true,
25+
'zookeeper.pravega.io': true,
26+
};

ui/src/app/applications/components/resource-details/resource-details.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => {
291291
<React.Fragment>
292292
<div className='resource-details__header'>
293293
<div style={{display: 'flex', flexDirection: 'column', marginRight: '15px', alignItems: 'center', fontSize: '12px'}}>
294-
<ResourceIcon kind={selectedNode.kind} />
294+
<ResourceIcon group={selectedNode.group} kind={selectedNode.kind} />
295295
{ResourceLabel({kind: selectedNode.kind})}
296296
</div>
297297
<h1>{selectedNode.name}</h1>

ui/src/app/applications/components/resource-icon.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
import * as React from 'react';
22
import {resourceIcons} from './resources';
3+
import {resourceIconGroups as resourceCustomizations} from './resource-customizations';
4+
import * as minimatch from 'minimatch';
35

4-
export const ResourceIcon = ({kind, customStyle}: {kind: string; customStyle?: React.CSSProperties}) => {
6+
export const ResourceIcon = ({group, kind, customStyle}: {group: string; kind: string; customStyle?: React.CSSProperties}) => {
57
if (kind === 'node') {
68
return <img src={'assets/images/infrastructure_components/' + kind + '.svg'} alt={kind} style={{padding: '2px', width: '40px', height: '32px', ...customStyle}} />;
79
}
8-
const i = resourceIcons.get(kind);
9-
if (i !== undefined) {
10-
return <img src={'assets/images/resources/' + i + '.svg'} alt={kind} style={{padding: '2px', width: '40px', height: '32px', ...customStyle}} />;
11-
}
1210
if (kind === 'Application') {
1311
return <i title={kind} className={`icon argo-icon-application`} style={customStyle} />;
1412
}
13+
if (!group) {
14+
const i = resourceIcons.get(kind);
15+
if (i !== undefined) {
16+
return <img src={'assets/images/resources/' + i + '.svg'} alt={kind} style={{padding: '2px', width: '40px', height: '32px', ...customStyle}} />;
17+
}
18+
} else {
19+
const matchedGroup = matchGroupToResource(group);
20+
if (matchedGroup) {
21+
return <img src={`assets/images/resources/${matchedGroup}/icon.svg`} alt={kind} style={{paddingBottom: '2px', width: '40px', height: '32px', ...customStyle}} />;
22+
}
23+
}
1524
const initials = kind.replace(/[a-z]/g, '');
1625
const n = initials.length;
1726
const style: React.CSSProperties = {
@@ -32,3 +41,22 @@ export const ResourceIcon = ({kind, customStyle}: {kind: string; customStyle?: R
3241
</div>
3342
);
3443
};
44+
45+
// Utility function to match group with possible wildcards in resourceCustomizations. If found, returns the matched key
46+
// as a path component (with '*' replaced by '_' if necessary), otherwise returns an empty string.
47+
function matchGroupToResource(group: string): string {
48+
// Check for an exact match
49+
if (group in resourceCustomizations) {
50+
return group;
51+
}
52+
53+
// Loop over the map keys to find a match using minimatch
54+
for (const key in resourceCustomizations) {
55+
if (key.includes('*') && minimatch(group, key)) {
56+
return key.replace(/\*/g, '_');
57+
}
58+
}
59+
60+
// Return an empty string if no match is found
61+
return '';
62+
}

0 commit comments

Comments
 (0)