Skip to content

Commit

Permalink
Feature: Searchbar on Kustomizations/HelmReleases/Source view (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzsak authored Apr 8, 2024
1 parent 521fe0f commit 50511a5
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 89 deletions.
29 changes: 3 additions & 26 deletions web/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,40 +38,17 @@ function App() {
localStorage.setItem("filters", JSON.stringify(filters));
}, [filters]);

const addFilter = (filter) => {
setFilters([...filters, filter]);
}

const filterValueByProperty = (property) => {
const filter = filters.find(f => f.property === property)
if (!filter) {
return ""
}

return filter.value
}

const deleteFilter = (filter) => {
setFilters(filters.filter(f => f.property !== filter.property))
}

const resetFilters = () => {
setFilters([])
}

return (
<>
<APIBackend capacitorClient={capacitorClient} store={store}/>
<StreamingBackend capacitorClient={capacitorClient} store={store}/>
<ToastNotifications store={store} handleNavigationSelect={handleNavigationSelect} />
<div className="max-w-6xl mx-auto">
<div className="my-16">
<FilterBar
<FilterBar
properties={["Service", "Namespace", "Domain"]}
filters={filters}
addFilter={addFilter}
deleteFilter={deleteFilter}
resetFilters={resetFilters}
filterValueByProperty={filterValueByProperty}
change={setFilters}
/>
</div>
<div className="grid grid-cols-1 gap-y-4 pb-32">
Expand Down
44 changes: 36 additions & 8 deletions web/src/FilterBar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import React, { useState, useRef, useEffect } from 'react';
import { FunnelIcon, XMarkIcon } from '@heroicons/react/24/outline'

function FilterBar({ filters, addFilter, deleteFilter, resetFilters, filterValueByProperty }) {
function FilterBar({ properties = [], filters, change }) {
const addFilter = (filter) => {
change([...filters, filter]);
}

const filterValueByProperty = (property) => {
const filter = filters.find(f => f.property === property)
if (!filter) {
return ""
}

return filter.value
}

const deleteFilter = (filter) => {
change(filters.filter(f => f.property !== filter.property))
}

const resetFilters = () => {
change([])
}

return (
<div className="w-full">
<div className="relative">
Expand All @@ -10,7 +31,7 @@ function FilterBar({ filters, addFilter, deleteFilter, resetFilters, filterValue
{filters.map(filter => (
<Filter key={filter.property + filter.value} filter={filter} deleteFilter={deleteFilter} />
))}
<FilterInput addFilter={addFilter} filterValueByProperty={filterValueByProperty} />
<FilterInput properties={properties} addFilter={addFilter} filterValueByProperty={filterValueByProperty} />
</div>
<div className="block w-full rounded-lg border-0 bg-white py-1.5 pl-10 pr-3 text-neutral-900 ring-1 ring-inset ring-neutral-300 placeholder:text-neutral-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
&nbsp;
Expand All @@ -29,7 +50,7 @@ function Filter(props) {
const { filter } = props;
return (
<span className="ml-1 text-blue-50 bg-blue-600 rounded-full pl-3 pr-1" aria-hidden="true">
<span>{filter.property}</span>: <span>{filter.value}</span>
<span>{filter.property}</span>{filter.property !== "Errors" && <span>: {filter.value}</span>}
<span className="ml-1 px-1 bg-blue-400 rounded-full ">
<XMarkIcon className="cursor-pointer text-white inline h-3 w-3" aria-hidden="true" onClick={() => props.deleteFilter(filter)}/>
</span>
Expand All @@ -41,8 +62,7 @@ function FilterInput(props) {
const [active, setActive] = useState(false)
const [property, setProperty] = useState("")
const [value, setValue] = useState("")
const properties=["Service", "Namespace", "Domain"]
const { addFilter, filterValueByProperty } = props;
const { properties, addFilter, filterValueByProperty } = props;
const inputRef = useRef(null);

const reset = () => {
Expand Down Expand Up @@ -76,7 +96,7 @@ function FilterInput(props) {
setActive(false);
if (value !== "") {
if (property === "") {
addFilter({property: "Service", value: value})
addFilter({property: properties[0], value: value})
} else {
addFilter({property, value})
}
Expand All @@ -92,7 +112,7 @@ function FilterInput(props) {
if (e.keyCode === 13){
setActive(false)
if (property === "") {
addFilter({property: "Service", value: value})
addFilter({property: properties[0], value: value})
} else {
addFilter({property, value})
}
Expand All @@ -117,7 +137,15 @@ function FilterInput(props) {
return (<li
key={p}
className="cursor-pointer hover:bg-blue-200"
onClick={() => { setProperty(p); setActive(false); }}>
onClick={() => {
if (p === "Errors") {
addFilter({ property: p, value: "true" })
return
}

setProperty(p);
setActive(false);
}}>
{p}
</li>)
})}
Expand Down
15 changes: 8 additions & 7 deletions web/src/HelmReleases.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useMemo, useState } from 'react';
import { HelmRelease } from "./HelmRelease"
import { filterResources } from './utils.js';
import FilterBar from './FilterBar.js';

export function HelmReleases(props) {
const { capacitorClient, helmReleases, targetReference, handleNavigationSelect } = props
const [filter, setFilter] = useState(false)
const [filters, setFilters] = useState([])
const sortedHelmReleases = useMemo(() => {
if (!helmReleases) {
return null;
Expand All @@ -13,15 +14,15 @@ export function HelmReleases(props) {
return [...helmReleases].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
}, [helmReleases]);

const filteredHelmReleases = filterResources(sortedHelmReleases, filter)
const filteredHelmReleases = filterResources(sortedHelmReleases, filters)

return (
<div className="space-y-4">
<button className={(filter ? "text-blue-50 bg-blue-600" : "bg-gray-50 text-gray-600") + " rounded-full px-3"}
onClick={() => setFilter(!filter)}
>
Filter errors
</button>
<FilterBar
properties={["Name", "Namespace", "Errors"]}
filters={filters}
change={setFilters}
/>
{
filteredHelmReleases?.map(helmRelease =>
<HelmRelease
Expand Down
15 changes: 8 additions & 7 deletions web/src/Kustomizations.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useMemo, useState } from 'react';
import { Kustomization } from './Kustomization.jsx'
import { filterResources } from './utils.js';
import FilterBar from './FilterBar.js';

export function Kustomizations(props) {
const { capacitorClient, fluxState, targetReference, handleNavigationSelect } = props
const [filter, setFilter] = useState(false)
const [filters, setFilters] = useState([])
const kustomizations = fluxState.kustomizations;

const sortedKustomizations = useMemo(() => {
Expand All @@ -15,15 +16,15 @@ export function Kustomizations(props) {
return [...kustomizations].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
}, [kustomizations]);

const filteredKustomizations = filterResources(sortedKustomizations, filter)
const filteredKustomizations = filterResources(sortedKustomizations, filters)

return (
<div className="space-y-4">
<button className={(filter ? "text-blue-50 bg-blue-600" : "bg-gray-50 text-gray-600") + " rounded-full px-3"}
onClick={() => setFilter(!filter)}
>
Filter errors
</button>
<FilterBar
properties={["Name", "Namespace", "Errors"]}
filters={filters}
change={setFilters}
/>
{
filteredKustomizations?.map(kustomization =>
<Kustomization
Expand Down
15 changes: 8 additions & 7 deletions web/src/Sources.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState, useMemo } from 'react';
import { filterResources } from './utils.js';
import { Source } from "./Source"
import FilterBar from "./FilterBar";

export function Sources(props) {
const { capacitorClient, fluxState, targetReference, handleNavigationSelect } = props
const [filter, setFilter] = useState(false)
const [filters, setFilters] = useState([])
const sortedSources = useMemo(() => {
const sources = [];
if (fluxState.ociRepositories) {
Expand All @@ -17,15 +18,15 @@ export function Sources(props) {
return [...sources].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
}, [fluxState]);

const filteredSources = filterResources(sortedSources, filter)
const filteredSources = filterResources(sortedSources, filters)

return (
<div className="space-y-4">
<button className={(filter ? "text-blue-50 bg-blue-600" : "bg-gray-50 text-gray-600") + " rounded-full px-3"}
onClick={() => setFilter(!filter)}
>
Filter errors
</button>
<FilterBar
properties={["Name", "Namespace", "Errors"]}
filters={filters}
change={setFilters}
/>
{
filteredSources?.map(source =>
<Source
Expand Down
79 changes: 45 additions & 34 deletions web/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,52 @@ export function findSource(sources, reconciler) {
source.metadata.namespace === namespace)
}

export function filterResources(resources, filterErrors) {
export function filterResources(resources, filters) {
let filteredResources = resources;
if (filterErrors) {
filteredResources = filteredResources.filter(resource => {
const readyConditions = jp.query(resource.status, '$..conditions[?(@.type=="Ready")]');
const readyCondition = readyConditions.length === 1 ? readyConditions[0] : undefined
const ready = readyCondition && readyConditions[0].status === "True"

const dependencyNotReady = readyCondition && readyCondition.reason === "DependencyNotReady"

const readyTransitionTime = readyCondition ? readyCondition.lastTransitionTime : undefined
const parsed = Date.parse(readyTransitionTime, "yyyy-MM-dd'T'HH:mm:ss");
const fiveMinutesAgo = new Date();
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
const stalled = fiveMinutesAgo > parsed

const reconcilingConditions = jp.query(resource.status, '$..conditions[?(@.type=="Reconciling")]');
const reconcilingCondition = reconcilingConditions.length === 1 ? reconcilingConditions[0] : undefined
const reconciling = reconcilingCondition && reconcilingCondition.status === "True"

const fetchFailedConditions = jp.query(resource.status, '$..conditions[?(@.type=="FetchFailed")]');
const fetchFailedCondition = fetchFailedConditions.length === 1 ? fetchFailedConditions[0] : undefined
const fetchFailed = fetchFailedCondition && fetchFailedCondition.status === "True"

if (resource.kind === 'GitRepository' || resource.kind === "OCIRepository" || resource.kind === "Bucket" || resource.kind === "HelmRepository" || resource.kind === "HelmChart") {
return fetchFailed
}

if (ready || ((reconciling || dependencyNotReady) && !stalled)) {
return false;
} else {
return true;
}
})
}
filters.forEach(filter => {
switch (filter.property) {
case 'Name':
filteredResources = filteredResources.filter(resource => resource.metadata.name.includes(filter.value))
break;
case 'Namespace':
filteredResources = filteredResources.filter(resource => resource.metadata.namespace.includes(filter.value))
break;
case 'Errors':
filteredResources = filteredResources.filter(resource => {
const readyConditions = jp.query(resource.status, '$..conditions[?(@.type=="Ready")]');
const readyCondition = readyConditions.length === 1 ? readyConditions[0] : undefined
const ready = readyCondition && readyConditions[0].status === "True"

const dependencyNotReady = readyCondition && readyCondition.reason === "DependencyNotReady"

const readyTransitionTime = readyCondition ? readyCondition.lastTransitionTime : undefined
const parsed = Date.parse(readyTransitionTime, "yyyy-MM-dd'T'HH:mm:ss");
const fiveMinutesAgo = new Date();
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
const stalled = fiveMinutesAgo > parsed

const reconcilingConditions = jp.query(resource.status, '$..conditions[?(@.type=="Reconciling")]');
const reconcilingCondition = reconcilingConditions.length === 1 ? reconcilingConditions[0] : undefined
const reconciling = reconcilingCondition && reconcilingCondition.status === "True"

const fetchFailedConditions = jp.query(resource.status, '$..conditions[?(@.type=="FetchFailed")]');
const fetchFailedCondition = fetchFailedConditions.length === 1 ? fetchFailedConditions[0] : undefined
const fetchFailed = fetchFailedCondition && fetchFailedCondition.status === "True"

if (resource.kind === 'GitRepository' || resource.kind === "OCIRepository" || resource.kind === "Bucket" || resource.kind === "HelmRepository" || resource.kind === "HelmChart") {
return fetchFailed
}

if (ready || ((reconciling || dependencyNotReady) && !stalled)) {
return false;
} else {
return true;
}
})
break;
default:
}
})

return filteredResources;
}

0 comments on commit 50511a5

Please sign in to comment.