Skip to content

Commit

Permalink
Merge pull request #156 from akvo/feature/155-implement-indexed-DB
Browse files Browse the repository at this point in the history
Feature/155 implement indexed db
  • Loading branch information
wayangalihpratama authored Aug 8, 2023
2 parents 7d75469 + 46b031f commit f5b0325
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 64 deletions.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"axios": "^1.2.0",
"d3-geo": "^3.0.1",
"d3-scale": "^4.0.2",
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.6",
"echarts": "^5.4.2",
"echarts-for-react": "^3.0.2",
"leaflet": "^1.9.3",
Expand Down
35 changes: 26 additions & 9 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,38 @@ import { Routes, Route, useLocation } from "react-router-dom";
import { Layout } from "./components";
import { Home, DashboardView, ErrorPage } from "./pages";
import { UIState } from "./state/ui";
import { api } from "./lib";
import { api, ds } from "./lib";

const App = () => {
const location = useLocation();

useEffect(() => {
const url = `chart/number_of_school`;
api
.get(url)
.then((res) => {
UIState.update((s) => {
s.schoolTotal = res?.data?.total;
});
// #TODO:: Fetch cursor here (Replace with correct value)
ds.saveCursor({ cursor: 456 });
//
const url = `/chart/number_of_school`;
// check indexed DB first
ds.getSource(url)
.then((cachedData) => {
if (!cachedData) {
api
.get(url)
.then((res) => {
ds.saveSource({ endpoint: url, data: res.data });
UIState.update((s) => {
s.schoolTotal = res?.data?.total;
});
})
.catch((e) => console.error(e));
} else {
UIState.update((s) => {
s.schoolTotal = cachedData.data.total;
});
}
})
.catch((e) => console.error(e));
.catch((e) => {
console.error("[Failed fetch indexed DB sources table]", e);
});
}, []);

return (
Expand Down
93 changes: 93 additions & 0 deletions frontend/src/lib/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Dexie from "dexie";

const dbName = "siwins";
const db = new Dexie(dbName);

db.version(1).stores({
sync: "++id, cursor",
sources: "++endpoint, data", // store resources of dropdown data
maps: "++endpoint, data", // store data of maps page
dashboards: "++endpoint, data", // store data of dashboard page
});

const checkDB = () =>
Dexie.exists(dbName)
.then((exists) => {
if (exists) {
console.info("Database exists");
} else {
console.info("Database doesn't exist");
}
})
.catch((e) => {
console.error(
"Oops, an error occurred when trying to check database existance"
);
console.error(e);
});

const truncateTables = () => {
db.sources.clear();
db.maps.clear();
db.dashboards.clear();
};

const getSource = async (endpoint) => {
const res = await db.sources.get({ endpoint });
if (!res) {
return null;
}
return {
...res,
data: JSON.parse(res.data),
};
};

const saveSource = ({ endpoint, data }) => {
return db.sources.put({ endpoint, data: JSON.stringify(data) });
};

const getMap = async (endpoint) => {
const res = await db.maps.get({ endpoint });
if (!res) {
return null;
}
return {
...res,
data: JSON.parse(res.data),
};
};

const saveMap = ({ endpoint, data }) => {
return db.maps.put({ endpoint, data: JSON.stringify(data) });
};

const getDashboard = async (endpoint) => {
const res = await db.dashboards.get({ endpoint });
if (!res) {
return null;
}
return {
...res,
data: JSON.parse(res.data),
};
};

const saveDashboard = ({ endpoint, data }) => {
return db.dashboards.put({ endpoint, data: JSON.stringify(data) });
};

const ds = {
checkDB,
truncateTables,
getSource,
saveSource,
getMap,
saveMap,
getDashboard,
saveDashboard,
getCursor: async () => await db.sync.get({ id: 1 }),
saveCursor: ({ cursor }) => db.sync.put({ id: 1, cursor }),
};

export default ds;
1 change: 1 addition & 0 deletions frontend/src/lib/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default as api } from "./api";
export { default as ds } from "./db";
62 changes: 42 additions & 20 deletions frontend/src/pages/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from "react";
import { Row, Col, Select, Breadcrumb } from "antd";
import { api } from "../../lib";
import { api, ds } from "../../lib";
import { UIState } from "../../state/ui";
import ChartVisual from "./components/ChartVisual";
import { Chart } from "../../components";
import AdvanceFilter from "../../components/filter";
import {
generateAdvanceFilterURL,
generateFilterURL,
sequentialPromise,
} from "../../util/utils";
import { generateAdvanceFilterURL, generateFilterURL } from "../../util/utils";
import { Link } from "react-router-dom";
import { orderBy } from "lodash";

Expand Down Expand Up @@ -63,32 +59,58 @@ const Dashboard = () => {
?.chartList
);
setPageLoading(true);
const apiCall = chartList?.map((chart) => {
let url = `chart/jmp-data/${chart?.path}`;
chartList?.forEach((chart) => {
let url = `/chart/jmp-data/${chart?.path}`;
url = generateAdvanceFilterURL(advanceSearchValue, url);
url = generateFilterURL(provinceFilterValue, url);
return api.get(url);
});
sequentialPromise(apiCall).then((res) => {
setData(res);
setPageLoading(false);
// ** fetch data from indexed DB first
ds.getDashboard(url)
.then(async (cachedData) => {
if (!cachedData) {
await api
.get(url)
.then((res) => {
ds.saveDashboard({ endpoint: url, data: res });
setData((prevData) => [...prevData, res]);
})
.catch((e) => {
console.error("[Error fetch JMP chart data]", e);
});
} else {
setData((prevData) => [...prevData, cachedData.data]);
}
})
.finally(() => {
setPageLoading(false);
});
});
}, [advanceSearchValue, provinceFilterValue]);

// generic bar chart
useEffect(() => {
if (!selectedIndicator) {
setBarChartData([]);
return;
}
let url = `chart/generic-bar/${selectedIndicator}`;
let url = `/chart/generic-bar/${selectedIndicator}`;
url = generateAdvanceFilterURL(advanceSearchValue, url);
url = generateFilterURL(provinceFilterValue, url);
api
.get(url)
.then((res) => {
setBarChartData(res.data);
})
.catch((e) => console.error(e));
// ** fetch data from indexed DB first
ds.getDashboard(url).then((cachedData) => {
if (!cachedData) {
api
.get(url)
.then((res) => {
ds.saveDashboard({ endpoint: url, data: res.data });
setBarChartData(res.data);
})
.catch((e) =>
console.error("[Error fetch Generic Bar chart data]", e)
);
} else {
setBarChartData(cachedData.data);
}
});
}, [selectedIndicator, advanceSearchValue, provinceFilterValue]);

const renderColumn = (cfg, index) => {
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/pages/dashboard/components/ChartVisual.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Row, Col, Card, Switch, Space, Popover } from "antd";
import { Chart } from "../../../components";
import { get } from "lodash";
import { InfoCircleOutlined } from "@ant-design/icons";
import { api } from "../../../lib";
import { api, ds } from "../../../lib";
import {
generateAdvanceFilterURL,
generateFilterURL,
Expand Down Expand Up @@ -32,9 +32,20 @@ const ChartVisual = ({ chartConfig, loading }) => {
(async () => {
const queryUrlPrefix = url.includes("?") ? "&" : "?";
url = `${url}${queryUrlPrefix}history=${showHistory}`;
const res = await api.get(url);
setLoading(false);
setHistoryData(res?.data?.data);
ds.getDashboard(url)
.then(async (cachedData) => {
if (!cachedData) {
await api.get(url).then((res) => {
ds.saveDashboard({ endpoint: url, data: res.data });
setHistoryData(res?.data?.data);
});
} else {
setHistoryData(cachedData.data.data);
}
})
.finally(() => {
setLoading(false);
});
})();
}
}, [showHistory, path, setLoading, advanceSearchValue, provinceFilterValue]);
Expand Down
97 changes: 75 additions & 22 deletions frontend/src/pages/dashboard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import Maps from "./Maps";
import Dashboard from "./Dashboard";
import ManageData from "./ManageData";
import { UIState } from "../../state/ui";
import { api } from "../../lib";
import { api, ds } from "../../lib";

const menuItems = [
{ label: "Maps", link: "/dashboard/maps", icon: <MapsIcon />, key: "1" },
Expand All @@ -30,37 +30,90 @@ const menuItems = [
},
];

const dropdownResourceURL = [
"/question?attribute=indicator",
"/question?attribute=advance_filter",
"/question?attribute=generic_bar_chart",
"/cascade/school_information?level=province",
"/cascade/school_information?level=school_type",
];

const DashboardView = () => {
const location = useLocation();
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(true);
const [fetchFromAPI, setFetchFromAPI] = useState(false);
const { schoolTotal } = UIState.useState((s) => s);

useEffect(() => {
Promise.all([
api.get("/question?attribute=indicator"),
api.get("/question?attribute=advance_filter"),
api.get("/question?attribute=generic_bar_chart"),
api.get("/cascade/school_information?level=province"),
api.get("/cascade/school_information?level=school_type"),
]).then((res) => {
const [
indicatorQuestions,
advanceFilterQuestions,
generic_bar_chart,
province,
school_type,
] = res;
UIState.update((s) => {
s.indicatorQuestions = indicatorQuestions?.data;
s.advanceFilterQuestions = advanceFilterQuestions?.data;
s.barChartQuestions = generic_bar_chart?.data;
s.provinceValues = province?.data;
s.schoolTypeValues = school_type?.data;
});
// ** fetch dropdown sources from indexed DB first
const dsApiCalls = dropdownResourceURL.map((url) => ds.getSource(url));
Promise.all(dsApiCalls).then((cachedData) => {
const nullInsideRes = cachedData.filter((x) => !x)?.length;
if (nullInsideRes) {
setFetchFromAPI(true);
} else {
const [
indicatorQuestions,
advanceFilterQuestions,
generic_bar_chart,
province,
school_type,
] = cachedData;
UIState.update((s) => {
s.indicatorQuestions = indicatorQuestions?.data;
s.advanceFilterQuestions = advanceFilterQuestions?.data;
s.barChartQuestions = generic_bar_chart?.data;
s.provinceValues = province?.data;
s.schoolTypeValues = school_type?.data;
});
}
});
}, []);

useEffect(() => {
// ** fetch from API if indexed DB not defined
if (fetchFromAPI) {
const apiCalls = dropdownResourceURL.map((url) => api.get(url));
Promise.all(apiCalls).then((res) => {
const [
indicatorQuestions,
advanceFilterQuestions,
generic_bar_chart,
province,
school_type,
] = res;
// save to indexed DB
ds.saveSource({
endpoint: indicatorQuestions.config.url,
data: indicatorQuestions.data,
});
ds.saveSource({
endpoint: advanceFilterQuestions.config.url,
data: advanceFilterQuestions.data,
});
ds.saveSource({
endpoint: generic_bar_chart.config.url,
data: generic_bar_chart.data,
});
ds.saveSource({ endpoint: province.config.url, data: province.data });
ds.saveSource({
endpoint: school_type.config.url,
data: school_type.data,
});
//
UIState.update((s) => {
s.indicatorQuestions = indicatorQuestions?.data;
s.advanceFilterQuestions = advanceFilterQuestions?.data;
s.barChartQuestions = generic_bar_chart?.data;
s.provinceValues = province?.data;
s.schoolTypeValues = school_type?.data;
});
setFetchFromAPI(false);
});
}
}, [fetchFromAPI]);

const handleOnClickMenu = ({ key }) => {
const link = menuItems.find((x) => x.key === key)?.link;
navigate(link);
Expand Down
Loading

0 comments on commit f5b0325

Please sign in to comment.