diff --git a/package-lock.json b/package-lock.json index 6fed2db..6433691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "codealike-plugin", - "version": "2.0.2", + "version": "2.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "codealike-plugin", - "version": "2.0.2", - "license": "GPL3", + "version": "2.0.4", + "license": "MIT", "dependencies": { "chart.js": "^3.2.1", "idb": "^6.1.2", @@ -7774,7 +7774,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -11020,13 +11019,15 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-node": { "version": "1.8.2", @@ -12290,7 +12291,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-prettier": { "version": "4.2.1", @@ -12362,7 +12364,8 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true + "dev": true, + "requires": {} }, "eslint-plugin-sort-keys-fix": { "version": "1.1.2", @@ -13757,7 +13760,8 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "29.2.0", @@ -15164,7 +15168,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -15661,7 +15664,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true + "dev": true, + "requires": {} }, "supports-color": { "version": "7.2.0", diff --git a/src/background.index.ts b/src/background.index.ts index 9314127..7868a5f 100644 --- a/src/background.index.ts +++ b/src/background.index.ts @@ -14,6 +14,7 @@ import { DebugTab } from './shared/db/types'; import { WAKE_UP_BACKGROUND } from './shared/messages'; import Port = chrome.runtime.Port; +import { manageLocalStoreSpace } from './background/services/data'; interface Service { name: string; @@ -30,6 +31,10 @@ const ASYNC_POLL_INTERVAL_MINUTES = 1; const ASYNC_STATS_INTERVAL_ALARM_NAME = 'send-stats'; const ASYNC_STATS_INTERVAL_MINUTES = 1; +const ASYNC_CLEAN_TIMELINE_ALARM_NAME = 'clean-timeline'; +const ASYNC_CLEAN_TIMELINE_MINUTES = 1440; // 24 hours * 60 minutes - one day +const DEFAULT_CUTOFF_DAYS = 60; + function findDebuggingTabIndexFromId(tabIdOrUrl: number | string | undefined) { if (tabIdOrUrl !== undefined) { for (let i = 0; i < debuggingTabs.length; i++) { @@ -78,6 +83,10 @@ const asyncPollAlarmHandler = async (): Promise => { const sendStatsAlarmHandler = async (): Promise => await sendWebActivityAutomatically(); +const cleanTimeLineAlarmHandler = async (): Promise => { + await manageLocalStoreSpace(DEFAULT_CUTOFF_DAYS) +} + const ChromeServiceDefinition: Array = [ { handler: asyncPollAlarmHandler, @@ -89,6 +98,11 @@ const ChromeServiceDefinition: Array = [ intervalInMinutes: ASYNC_STATS_INTERVAL_MINUTES, name: ASYNC_STATS_INTERVAL_ALARM_NAME, }, + { + handler: cleanTimeLineAlarmHandler, + intervalInMinutes: ASYNC_CLEAN_TIMELINE_MINUTES, + name: ASYNC_CLEAN_TIMELINE_ALARM_NAME + } ]; ChromeServiceDefinition.forEach((service) => { diff --git a/src/background/services/data.ts b/src/background/services/data.ts new file mode 100644 index 0000000..01a648a --- /dev/null +++ b/src/background/services/data.ts @@ -0,0 +1,69 @@ +import { connect, TimeTrackerStoreTables } from '../../shared/db/idb'; +import { logMessage } from '../tables/logs'; + +const getIdsToDelete = async (cutOffDate: Date) => { + await logMessage(`Cut Off Date: ${cutOffDate.getTime()}`); + const db = await connect(); + const timeline = await db.getAll( + TimeTrackerStoreTables.Timeline, + ); + const idsToDelete: number[] = []; + + for (const data of timeline) { + const recordDate = new Date(data.date).getTime(); + if (recordDate < cutOffDate.getTime()) { + const id: number = data.id as number; + // Should be deleted + idsToDelete.push(id); + } + } + + return idsToDelete; +}; + +const deleteRecordsById = async (idsToDelete: number[]): Promise => { + try { + + const db = await connect(); + const transaction = db.transaction( + TimeTrackerStoreTables.Timeline, + 'readwrite', + ); + const store = transaction.objectStore(TimeTrackerStoreTables.Timeline); + + for (const id of idsToDelete) { + // Using any as delete function expects string but we store id as number + // this throws lint error + const delId: any = id; + await store.delete(delId); + } + + await transaction.done; + await logMessage('Records deleted successfully.'); + return 'Records deleted successfully.'; + } catch (error) { + console.error('Error deleting IDB records:', error); + return 'Failed to delete records.'; + } +}; + +const manageLocalStoreSpace = async ( + defaultCutOff: number, +): Promise => { + await logMessage(`Handling Old Data Cleanup`); + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - defaultCutOff); + const idsToDelete = await getIdsToDelete(cutoffDate); + + await logMessage( + `Records to Delete: ${JSON.stringify(idsToDelete, null, 2)}`, + ); + + if (idsToDelete.length > 0) { + return await deleteRecordsById(idsToDelete); + } + + return `No timeline data available for keeping beyond the ${defaultCutOff} days period.` +}; + +export { manageLocalStoreSpace }; diff --git a/src/popup/components/CleanUpTimeLineStorage/CleanUpTimeLineStorage.tsx b/src/popup/components/CleanUpTimeLineStorage/CleanUpTimeLineStorage.tsx new file mode 100644 index 0000000..20e1830 --- /dev/null +++ b/src/popup/components/CleanUpTimeLineStorage/CleanUpTimeLineStorage.tsx @@ -0,0 +1,101 @@ +import * as React from "react"; +import { Panel, PanelBody, PanelHeader } from "../../../blocks/Panel"; +import { Button, ButtonType } from "../../../blocks/Button"; +import { handleClearTimeLineData } from "../../../shared/db/helper"; +import { Input } from "../../../blocks/Input"; +import { usePopupContext } from "../../hooks/PopupContext"; + +export const CleanUpTimeLineStorage: React.FC = () => { + const { settings, updateSettings } = usePopupContext() + const [state, setState] = React.useState<{ + cutOffDate: number | string + status: boolean, + statusText: string, + }>({ + cutOffDate: '', + status: false, + statusText: '', + }); + + const handleClearData = React.useCallback(() => { + const days = Number(state.cutOffDate); + if (days < 1) { + setState((prev) => ({ + ...prev, + status: true, + statusText: 'Cut Off Date cannot be lower than 1' + })); + return; + } + + handleClearTimeLineData(days) + .then((res) => { + // Update state + setState((prev) => ({ + ...prev, + status: true, + statusText: res, + })); + + // Update settings + updateSettings({ + timeLineCleanUpDays: days + }); + }) + }, [state, updateSettings]); + + const handleCutOffDate = React.useCallback( + (e: React.ChangeEvent) => { + setState((prev) => ({ + ...prev, + cutOffDate: Number(e.target.value), + })); + }, + [], + ); + + React.useEffect(() => { + const { timeLineCleanUpDays } = settings + if (timeLineCleanUpDays) { + setState((prev) => ({ + ...prev, + cutOffDate: timeLineCleanUpDays + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const { cutOffDate, status, statusText } = state; + return ( + + Clean Up Old Timeline Data + +

Enter the number of days you wish to retain your data. Data older than this period will be removed.

+
+ + +
+

After clicking the Clear button, only the timeline data from your local storage will be cleared. The synced data will remain in the server.

+
+ {status + && (

{statusText}

) + } +
+
+
+ ) +} \ No newline at end of file diff --git a/src/popup/pages/PreferencesPage.tsx b/src/popup/pages/PreferencesPage.tsx index 4633116..b540799 100644 --- a/src/popup/pages/PreferencesPage.tsx +++ b/src/popup/pages/PreferencesPage.tsx @@ -8,6 +8,7 @@ import { Input } from './../../blocks/Input'; import {IgnoredDomainSetting} from '../components/IgnoredDomainsSetting/IgnoredDomainSetting'; import { WhitelistDomainSetting } from '../components/WhitelistDomainsSetting/WhitelistDomainSetting'; import {UserTokenSetting} from "../components/UserTokenSetting/UserTokenSetting"; +import { CleanUpTimeLineStorage } from '../components/CleanUpTimeLineStorage/CleanUpTimeLineStorage'; export const PreferencesPage: FC = () => { const [isWhitelistShown, hideWhitelist] = React.useState(true); @@ -45,6 +46,7 @@ export const PreferencesPage: FC = () => { {(isWhitelistShown ? : )} + ); }; diff --git a/src/shared/db/helper.ts b/src/shared/db/helper.ts new file mode 100644 index 0000000..724198b --- /dev/null +++ b/src/shared/db/helper.ts @@ -0,0 +1,5 @@ +import { manageLocalStoreSpace } from "../../background/services/data"; + +export const handleClearTimeLineData = async (days: number) => { + return await manageLocalStoreSpace(days) +}; \ No newline at end of file diff --git a/src/shared/db/types.ts b/src/shared/db/types.ts index e31518c..99e69ee 100644 --- a/src/shared/db/types.ts +++ b/src/shared/db/types.ts @@ -32,6 +32,7 @@ export interface TimelineRecord { secure: boolean; activityPeriodStart: number; activityPeriodEnd: number; + id?: number; } export type ActiveTabState = { @@ -61,6 +62,7 @@ export interface Preferences { limits: Record; displayTimeOnBadge: boolean; lastUpdateStats?: Statistics; + timeLineCleanUpDays?: number; } export interface Statistics {