diff --git a/app-catalog/.gitignore b/app-catalog/.gitignore
index 4d29575..6f9dae1 100644
--- a/app-catalog/.gitignore
+++ b/app-catalog/.gitignore
@@ -5,6 +5,8 @@
/.pnp
.pnp.js
+/dist
+
# testing
/coverage
diff --git a/app-catalog/src/api/charts.tsx b/app-catalog/src/api/charts.tsx
index 171cef3..94f5cef 100644
--- a/app-catalog/src/api/charts.tsx
+++ b/app-catalog/src/api/charts.tsx
@@ -1,23 +1,32 @@
import { PAGE_OFFSET_COUNT_FOR_CHARTS } from '../components/charts/List';
-export function fetchChartsFromArtifact(
+export async function fetchChartsFromArtifact(
search: string = '',
+ verified: boolean,
category: { title: string; value: number },
page: number,
limit: number = PAGE_OFFSET_COUNT_FOR_CHARTS
) {
- if (!category || category.value === 0) {
- return fetch(
- `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&sort=relevance&facets=true&limit=${limit}&offset=${
- (page - 1) * limit
- }`
- ).then(response => response.json());
+ // note: we are currently defaulting to search by verified and official as default
+ const url = new URL('https://artifacthub.io/api/v1/packages/search');
+ url.searchParams.set('offset', ((page - 1) * limit).toString());
+ url.searchParams.set('limit', limit.toString());
+ url.searchParams.set('facets', 'true');
+ url.searchParams.set('kind', '0');
+ url.searchParams.set('ts_query_web', search);
+ if (category.value) {
+ url.searchParams.set('category', category.value.toString());
}
- return fetch(
- `https://artifacthub.io/api/v1/packages/search?kind=0&ts_query_web=${search}&category=${
- category.value
- }&sort=relevance&facets=true&limit=${limit}&offset=${(page - 1) * limit}`
- ).then(response => response.json());
+ url.searchParams.set('sort', 'relevance');
+ url.searchParams.set('deprecated', 'false');
+ url.searchParams.set('verified_publisher', verified.toString());
+
+ const response = await fetch(url.toString());
+ const total = response.headers.get('pagination-total-count');
+
+ const dataResponse = await response.json();
+
+ return { dataResponse, total };
}
export function fetchChartDetailFromArtifact(chartName: string, repoName: string) {
diff --git a/app-catalog/src/components/charts/AppCatalogTitle.stories.tsx b/app-catalog/src/components/charts/AppCatalogTitle.stories.tsx
new file mode 100644
index 0000000..889bd92
--- /dev/null
+++ b/app-catalog/src/components/charts/AppCatalogTitle.stories.tsx
@@ -0,0 +1,21 @@
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { AppCatalogTitle } from './AppCatalogTitle';
+
+export default {
+ title: 'Components/AppCatalogTitle',
+ component: AppCatalogTitle,
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+} as Meta;
+
+const Template: Story = args => ;
+
+export const Title = Template.bind({});
+Title.args = {};
diff --git a/app-catalog/src/components/charts/AppCatalogTitle.tsx b/app-catalog/src/components/charts/AppCatalogTitle.tsx
new file mode 100644
index 0000000..7109825
--- /dev/null
+++ b/app-catalog/src/components/charts/AppCatalogTitle.tsx
@@ -0,0 +1,17 @@
+import { Box, Typography } from '@mui/material';
+import { SettingsLink } from './SettingsLink';
+
+export function AppCatalogTitle() {
+ return (
+
+
+ Applications
+
+
+
+ );
+}
diff --git a/app-catalog/src/components/charts/ChartsList.stories.tsx b/app-catalog/src/components/charts/ChartsList.stories.tsx
index 435e471..fd46863 100644
--- a/app-catalog/src/components/charts/ChartsList.stories.tsx
+++ b/app-catalog/src/components/charts/ChartsList.stories.tsx
@@ -32,27 +32,62 @@ const mockCharts = [
url: 'https://example2.com',
},
},
+ {
+ name: 'MockChart3',
+ version: '1.0',
+ description: 'This is a mock chart description.',
+ logo_image_id: 'zzzzz-3fce-4b63-bbf3-b9f649512a86',
+ repository: {
+ name: 'MockRepoy',
+ url: 'https://exampley.com',
+ verified_publisher: true,
+ },
+ },
+ {
+ name: 'MockChart4',
+ version: '1.1',
+ description:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor.',
+ logo_image_id: 'zzzzz-28b3-4ee8-98a2-30e00abf9f02',
+ repository: {
+ name: 'MockRepo2y',
+ url: 'https://example2y.com',
+ verified_publisher: true,
+ },
+ },
];
-const initialState = {
+const initialStateTrue = {
config: {
+ showOnlyVerified: true,
settings: {
tableRowsPerPageOptions: [15, 25, 50],
},
},
};
-const mockStore = configureStore({
- reducer: (state = initialState) => state,
-});
+const initialStateFalse = {
+ config: {
+ showOnlyVerified: false,
+ settings: {
+ tableRowsPerPageOptions: [15, 25, 50],
+ },
+ },
+};
+
+const Template: Story = ({ initialState, ...args }) => {
+ const mockStore = configureStore({
+ reducer: (state = initialState) => state,
+ });
-const Template: Story = args => (
-
-
-
-
-
-);
+ return (
+
+
+
+
+
+ );
+};
export const EmptyCharts = Template.bind({});
EmptyCharts.args = {
@@ -81,3 +116,33 @@ SomeCharts.args = {
],
}),
};
+
+export const WithShowOnlyVerifiedTrue = Template.bind({});
+WithShowOnlyVerifiedTrue.args = {
+ initialState: initialStateTrue,
+ fetchCharts: () =>
+ Promise.resolve({
+ packages: mockCharts,
+ facets: [
+ {
+ title: 'Category',
+ options: [{ name: 'All', total: 0 }],
+ },
+ ],
+ }),
+};
+
+export const WithShowOnlyVerifiedFalse = Template.bind({});
+WithShowOnlyVerifiedFalse.args = {
+ initialState: initialStateFalse,
+ fetchCharts: () =>
+ Promise.resolve({
+ packages: mockCharts,
+ facets: [
+ {
+ title: 'Category',
+ options: [{ name: 'All', total: 0 }],
+ },
+ ],
+ }),
+};
diff --git a/app-catalog/src/components/charts/List.tsx b/app-catalog/src/components/charts/List.tsx
index c02eb1a..b9cf36c 100644
--- a/app-catalog/src/components/charts/List.tsx
+++ b/app-catalog/src/components/charts/List.tsx
@@ -1,4 +1,5 @@
import { Icon } from '@iconify/react';
+import { ConfigStore } from '@kinvolk/headlamp-plugin/lib';
import {
Link as RouterLink,
Loader,
@@ -21,10 +22,20 @@ import { Autocomplete, Pagination } from '@mui/material';
import { useEffect, useState } from 'react';
//import { jsonToYAML, yamlToJSON } from '../../helpers';
import { fetchChartsFromArtifact } from '../../api/charts';
-import CNCFLight from './cncf-icon-color.svg';
+import { AppCatalogTitle } from './AppCatalogTitle';
//import { createRelease } from '../../api/releases';
import { EditorDialog } from './EditorDialog';
+interface AppCatalogConfig {
+ /**
+ * Show only verified packages. If set to false shows all the packages
+ */
+ showOnlyVerified: boolean;
+}
+
+export const store = new ConfigStore('app-catalog');
+const useStoreConfig = store.useConfig();
+
export const PAGE_OFFSET_COUNT_FOR_CHARTS = 9;
export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) {
@@ -39,7 +50,7 @@ export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) {
{ title: 'Storage', value: 7 },
{ title: 'Streaming and messaging', value: 8 },
];
- const [charts, setCharts] = useState(null);
+ const [charts, setCharts] = useState(null);
const [openEditor, setEditorOpen] = useState(false);
const [page, setPage] = useState(1);
const [chartsTotalCount, setChartsTotalCount] = useState(0);
@@ -47,45 +58,43 @@ export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) {
const [search, setSearch] = useState('');
const [selectedChartForInstall, setSelectedChartForInstall] = useState(null);
- useEffect(() => {
- setCharts(null);
- fetchCharts(search, chartCategory, page).then(response => {
- setCharts(response.packages);
- const facets = response.facets;
- const categoryOptions = facets.find(
- (facet: {
- title: string;
- options: {
- name: string;
- total: number;
- }[];
- }) => facet.title === 'Category'
- ).options;
- if (chartCategory.title === 'All') {
- const totalCount = categoryOptions.reduce(
- (
- acc: number,
- option: {
- name: string;
- total: number;
- }
- ) => acc + option.total,
- 0
- );
- setChartsTotalCount(totalCount);
- return;
- }
- const totalCount = categoryOptions.find(
- (option: { name: string; total: number }) => option.name === chartCategory?.title
- ).total;
- setChartsTotalCount(totalCount);
- });
- }, [page, chartCategory, search]);
+ // note: since we default to true for showOnlyVerified and the settings page is not accessible from anywhere else but the list comp
+ // we must have the default value here and have it imported for use in the settings tab
+ const config = useStoreConfig();
+ const showOnlyVerified = config?.showOnlyVerified ?? true;
+ // note: When the users changes the chartCategory or search, then we always go back to the first page
useEffect(() => {
setPage(1);
}, [chartCategory, search]);
+ // note: When the page changes, we fetch the charts, this will run as a reaction to the previous useEffect
+ useEffect(
+ function fetchChartsOnPageChange() {
+ setCharts(null);
+
+ store.set({ showOnlyVerified: showOnlyVerified });
+
+ if (props.showOnlyVerified!!) {
+ store.set({ showOnlyVerified: props.showOnlyVerified });
+ }
+
+ async function fetchData() {
+ try {
+ const response: any = await fetchCharts(search, showOnlyVerified, chartCategory, page);
+ setCharts(response.dataResponse.packages);
+ setChartsTotalCount(parseInt(response.total));
+ } catch (err) {
+ console.error('Error fetching charts', err);
+ setCharts([]);
+ }
+ }
+
+ fetchData();
+ },
+ [page, chartCategory, search, showOnlyVerified]
+ );
+
function Search() {
return (
setEditorOpen(open)}
/>
- , ]} />
+ } actions={[, ]} />
{!charts ? (
) : (
- charts.map(chart => {
- return (
-
-
-
-
+
+ {charts.map(chart => {
+ return (
+
+
- {(chart.cncf || chart.repository.cncf) && (
-
-
-
- )}
- {(chart.official || chart.repository.official) && (
-
-
-
- )}
- {chart.repository.verified_publisher && (
-
-
-
- )}
+
+
+ {(chart.cncf || chart.repository.cncf) && (
+
+
+
+ )}
+ {(chart.official || chart.repository.official) && (
+
+
+
+ )}
+ {chart.repository.verified_publisher && (
+
+
+
+ )}
+
-
-
-
-
-
-
- {chart.name}
-
-
-
-
-
- v{chart.version}
-
- {chart?.repository?.name || ''}
+
+
+
+ {chart.name}
+
+
-
-
-
-
- {chart?.description?.slice(0, 100)}
- {chart?.description?.length > 100 && (
-
- …
+
+ v{chart.version}
+
+
+ {chart?.repository?.name || ''}
- )}
-
-
-
-
-
+
+
+
+ {chart?.description?.slice(0, 100)}
+ {chart?.description?.length > 100 && (
+
+ …
+
+ )}
+
+
+
+ {
- setSelectedChartForInstall(chart);
- setEditorOpen(true);
+ justifyContent: 'space-between',
+ padding: '14px',
}}
>
- Install
-
-
- Learn More
-
-
-
-
- );
- })
+
+
+ Learn More
+
+
+
+
+ );
+ })}
+
)}
{charts && charts.length !== 0 && (
@@ -318,7 +355,7 @@ export function ChartsList({ fetchCharts = fetchChartsFromArtifact }) {
size="large"
shape="rounded"
page={page}
- count={Math.floor(chartsTotalCount / PAGE_OFFSET_COUNT_FOR_CHARTS)}
+ count={Math.ceil(chartsTotalCount / PAGE_OFFSET_COUNT_FOR_CHARTS)}
color="primary"
onChange={(e, page: number) => {
setPage(page);
diff --git a/app-catalog/src/components/charts/SettingsLink.stories.tsx b/app-catalog/src/components/charts/SettingsLink.stories.tsx
new file mode 100644
index 0000000..7bf82de
--- /dev/null
+++ b/app-catalog/src/components/charts/SettingsLink.stories.tsx
@@ -0,0 +1,21 @@
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { SettingsLink } from './SettingsLink';
+
+export default {
+ title: 'Components/SettingsLink',
+ component: SettingsLink,
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+} as Meta;
+
+const Template: Story = args => ;
+
+export const Title = Template.bind({});
+Title.args = {};
diff --git a/app-catalog/src/components/charts/SettingsLink.tsx b/app-catalog/src/components/charts/SettingsLink.tsx
new file mode 100644
index 0000000..a871f7f
--- /dev/null
+++ b/app-catalog/src/components/charts/SettingsLink.tsx
@@ -0,0 +1,20 @@
+import { Link as RouterLink } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
+import { Typography, useTheme } from '@mui/material';
+
+export function SettingsLink() {
+ const theme = useTheme();
+
+ return (
+
+
+ Settings
+
+
+ );
+}
diff --git a/app-catalog/src/components/charts/__snapshots__/AppCatalogTitle.stories.storyshot b/app-catalog/src/components/charts/__snapshots__/AppCatalogTitle.stories.storyshot
new file mode 100644
index 0000000..a2703d5
--- /dev/null
+++ b/app-catalog/src/components/charts/__snapshots__/AppCatalogTitle.stories.storyshot
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Components/AppCatalogTitle Title 1`] = `
+
+`;
diff --git a/app-catalog/src/components/charts/__snapshots__/SettingsLink.stories.storyshot b/app-catalog/src/components/charts/__snapshots__/SettingsLink.stories.storyshot
new file mode 100644
index 0000000..6817eb9
--- /dev/null
+++ b/app-catalog/src/components/charts/__snapshots__/SettingsLink.stories.storyshot
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Components/SettingsLink Title 1`] = `
+
+`;
diff --git a/app-catalog/src/components/settings/AppCatalogSettings.stories.tsx b/app-catalog/src/components/settings/AppCatalogSettings.stories.tsx
new file mode 100644
index 0000000..f8683ee
--- /dev/null
+++ b/app-catalog/src/components/settings/AppCatalogSettings.stories.tsx
@@ -0,0 +1,30 @@
+import { createTheme, ThemeProvider } from '@mui/material/styles';
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { AppCatalogSettings, AppCatalogSettingsProps } from './AppCatalogSettings';
+
+export default {
+ title: 'Components/AppCatalogSettings',
+ component: AppCatalogSettings,
+} as Meta;
+
+const theme = createTheme();
+
+const Template: Story = args => (
+
+
+
+);
+
+export const Default = Template.bind({});
+Default.args = {};
+
+export const ShowOnlyVerifiedTrue = Template.bind({});
+ShowOnlyVerifiedTrue.args = {
+ initialConfig: { showOnlyVerified: true },
+};
+
+export const ShowOnlyVerifiedFalse = Template.bind({});
+ShowOnlyVerifiedFalse.args = {
+ initialConfig: { showOnlyVerified: false },
+};
diff --git a/app-catalog/src/components/settings/AppCatalogSettings.tsx b/app-catalog/src/components/settings/AppCatalogSettings.tsx
new file mode 100644
index 0000000..c292b39
--- /dev/null
+++ b/app-catalog/src/components/settings/AppCatalogSettings.tsx
@@ -0,0 +1,55 @@
+import { HoverInfoLabel, NameValueTable } from '@kinvolk/headlamp-plugin/lib/components/common';
+import { Box, Typography } from '@mui/material';
+import { useState } from 'react';
+import { store } from '../charts/List';
+import { EnableSwitch } from './EnableSwitch';
+
+export interface AppCatalogSettingsProps {
+ initialConfig?: { showOnlyVerified: boolean };
+}
+
+export function AppCatalogSettings({ initialConfig }: AppCatalogSettingsProps) {
+ const config = initialConfig || store.get();
+
+ const [currentConfig, setCurrentConfig] = useState(config);
+
+ function handleSave(value) {
+ const updatedConfig = { showOnlyVerified: value };
+ store.set(updatedConfig);
+ setCurrentConfig(store.get());
+ return;
+ }
+
+ function toggleShowOnlyVerified() {
+ const newShowOnlyVerified = currentConfig?.showOnlyVerified;
+ handleSave(!newShowOnlyVerified);
+ }
+
+ return (
+
+ App Catalog Settings
+
+ ),
+ value: (
+
+ ),
+ },
+ ]}
+ />
+
+ );
+}
diff --git a/app-catalog/src/components/settings/EnableSwitch.Stories.tsx b/app-catalog/src/components/settings/EnableSwitch.Stories.tsx
new file mode 100644
index 0000000..4a63a65
--- /dev/null
+++ b/app-catalog/src/components/settings/EnableSwitch.Stories.tsx
@@ -0,0 +1,20 @@
+import { Meta, Story } from '@storybook/react';
+import React from 'react';
+import { EnableSwitch } from './EnableSwitch';
+
+export default {
+ title: 'Components/EnableSwitch',
+ component: EnableSwitch,
+} as Meta;
+
+const Template: Story = args => ;
+
+export const CheckedTrue = Template.bind({});
+CheckedTrue.args = {
+ checked: true,
+};
+
+export const CheckedFalse = Template.bind({});
+CheckedFalse.args = {
+ checked: false,
+};
diff --git a/app-catalog/src/components/settings/EnableSwitch.tsx b/app-catalog/src/components/settings/EnableSwitch.tsx
new file mode 100644
index 0000000..2f30fcf
--- /dev/null
+++ b/app-catalog/src/components/settings/EnableSwitch.tsx
@@ -0,0 +1,60 @@
+import { Switch, SwitchProps, useTheme } from '@mui/material';
+
+export function EnableSwitch(props: SwitchProps) {
+ const theme = useTheme();
+
+ return (
+
+ );
+}
diff --git a/app-catalog/src/index.tsx b/app-catalog/src/index.tsx
index ca3c1e7..dc019e2 100644
--- a/app-catalog/src/index.tsx
+++ b/app-catalog/src/index.tsx
@@ -1,4 +1,9 @@
-import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib';
+import {
+ registerPluginSettings,
+ registerRoute,
+ registerSidebarEntry,
+} from '@kinvolk/headlamp-plugin/lib';
+import { AppCatalogSettings } from '../src/components/settings/AppCatalogSettings';
import ChartDetails from './components/charts/Details';
import { ChartsList } from './components/charts/List';
import ReleaseDetail from './components/releases/Detail';
@@ -91,4 +96,14 @@ if (isElectron()) {
exact: true,
component: () => ,
});
+
+ registerRoute({
+ path: '/settings/plugins/app-catalog',
+ sidebar: 'Charts',
+ name: 'App Catalog',
+ exact: true,
+ component: () => ,
+ });
}
+
+registerPluginSettings('app-catalog', AppCatalogSettings, false);