Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

## [Unreleased]

## [1.11.0] - 2026-02-27

### Added

- **Dashboard Hub Banner** - Non-intrusive top banner in the Dashboard encouraging users to connect to CodeClocker Hub:
- Shown when no API key is configured
- Shown when subscription has expired, encouraging renewal to keep syncing data
- Hidden when user has an active Hub subscription
- Clickable links to connect or renew directly from the banner

## [1.10.0] - 2026-02-22

### Added

- **Project Timeline Heatmap** - Gantt-style heatmap in the Dashboard showing when each project was worked on over time:
Expand Down Expand Up @@ -213,7 +225,9 @@

- Support IntelliJ Platform 2024.3.5

[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.9.0...HEAD
[Unreleased]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.11.0...HEAD
[1.11.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.10.0...v1.11.0
[1.10.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.7.0...v1.8.0
[1.7.0]: https://github.com/codeclocker/codeclocker-intellij-plugin/compare/v1.6.0...v1.7.0
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,8 @@ CodeClocker is an IntelliJ IDEA plugin that tracks coding time and activity. It
## Code Style

- Only add comments for complex logic; avoid commenting obvious code

## Release Process

1. Update `pluginVersion` in `gradle.properties`
2. Add release note in `CHANGELOG.md`
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pluginGroup = com.codeclocker
pluginName = CodeClocker
pluginRepositoryUrl = https://github.com/codeclocker/codeclocker-intellij-plugin
# SemVer format -> https://semver.org
pluginVersion = 1.9.0
pluginVersion = 1.11.0

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 252
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package com.codeclocker.plugin.intellij.dashboard;

import static com.codeclocker.plugin.intellij.HubHost.HUB_UI_HOST;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import com.codeclocker.plugin.intellij.apikey.ApiKeyLifecycle;
import com.codeclocker.plugin.intellij.apikey.ApiKeyPersistence;
import com.codeclocker.plugin.intellij.apikey.EnterApiKeyAction;
import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.DashboardData;
import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.ProjectBreakdownEntry;
import com.codeclocker.plugin.intellij.dashboard.DashboardDataService.ProjectTimelineData;
Expand All @@ -13,21 +19,25 @@
import com.codeclocker.plugin.intellij.dashboard.ui.StreakCardPanel;
import com.codeclocker.plugin.intellij.dashboard.ui.TimePeriodSelectorPanel;
import com.intellij.icons.AllIcons;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.ui.HyperlinkLabel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.JBUI;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
Expand All @@ -51,10 +61,18 @@ public class DashboardPanel extends JPanel implements Disposable {
private final ActivityTimelinePanel activityTimeline;
private final ProjectTimelineGanttPanel projectTimelinePanel;
private final AllProjectsPanel allProjectsPanel;
private final JPanel infoBanner;
private final JLabel bannerMessageLabel;
private final HyperlinkLabel bannerLink;

public DashboardPanel() {
setLayout(new BorderLayout());

// Hub connection banner
this.bannerMessageLabel = new JLabel();
this.bannerLink = new HyperlinkLabel();
this.infoBanner = createInfoBanner();

// Header: period selector + refresh button
JPanel headerPanel = new JPanel(new BorderLayout());
periodSelector = new TimePeriodSelectorPanel(this::onPeriodChanged);
Expand All @@ -73,7 +91,11 @@ public void actionPerformed(@NotNull AnActionEvent e) {
toolbar.setTargetComponent(this);
headerPanel.add(toolbar.getComponent(), BorderLayout.EAST);

add(headerPanel, BorderLayout.NORTH);
JPanel topPanel = new JPanel();
topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS));
topPanel.add(infoBanner);
topPanel.add(headerPanel);
add(topPanel, BorderLayout.NORTH);

// Scrollable content
JPanel contentPanel = new JPanel();
Expand Down Expand Up @@ -162,6 +184,8 @@ private void refreshData() {
linesAddedCard.setLoading(true);
linesRemovedCard.setLoading(true);

updateInfoBanner();

ApplicationManager.getApplication()
.executeOnPooledThread(
() -> {
Expand Down Expand Up @@ -239,6 +263,65 @@ private String formatNumber(long value) {
return String.valueOf(value);
}

private JPanel createInfoBanner() {
JPanel banner = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 4));
banner.setBackground(new JBColor(new Color(255, 248, 225), new Color(62, 53, 32)));
banner.setBorder(
JBUI.Borders.customLine(
new JBColor(new Color(255, 183, 77), new Color(139, 105, 20)), 0, 0, 1, 0));

JLabel infoIcon = new JLabel(AllIcons.General.Information);
banner.add(infoIcon);
banner.add(bannerMessageLabel);
banner.add(bannerLink);

bannerLink.addHyperlinkListener(
e -> {
if (javax.swing.event.HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey());
boolean subscriptionExpired = ApiKeyLifecycle.isActivityDataStoppedBeingCollected();

if (hasApiKey && subscriptionExpired) {
BrowserUtil.browse(HUB_UI_HOST + "/payment");
} else {
EnterApiKeyAction.showAction();
}
}
});

banner.setVisible(false);
return banner;
}

private void updateInfoBanner() {
ApplicationManager.getApplication()
.invokeLater(
() -> {
try {
boolean hasApiKey = isNotBlank(ApiKeyPersistence.getApiKey());
boolean subscriptionExpired = ApiKeyLifecycle.isActivityDataStoppedBeingCollected();

if (hasApiKey && !subscriptionExpired) {
infoBanner.setVisible(false);
return;
}

if (hasApiKey) {
bannerMessageLabel.setText(
"Your subscription has expired. New activity data is no longer being synced.");
bannerLink.setHyperlinkText("Renew subscription to keep your data safe");
} else {
bannerMessageLabel.setText("Your coding data is stored locally.");
bannerLink.setHyperlinkText("Connect to Hub to save your data forever");
}

infoBanner.setVisible(true);
} catch (Exception e) {
infoBanner.setVisible(false);
}
});
}

@Override
public void dispose() {}
}
Loading