Skip to content
Draft
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions docs/development/plugins/functionality/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,14 @@ Add custom sections to the project overview page with
These sections appear in the project's main overview area.

Example plugin: [How to customize projects](https://github.com/kubernetes-sigs/headlamp/tree/main/plugins/examples/projects)

### Activities

Activity is a Headlamp feature that allows you to create resizable popup windows.
For example when you click on a resource (like a Pod or ReplicaSet), the details will open in Activity.

![screenshot of an activity example](./images/activity-example.png)

You can create and update Actitivities from plugins using [Activity API](../../api/components/activity/Activity/variables/Activity.md)

Check the [example plugin](https://github.com/kubernetes-sigs/headlamp/tree/main/plugins/examples/activity) for the full code.
3 changes: 2 additions & 1 deletion frontend/src/components/activity/Activity.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { Meta, StoryFn } from '@storybook/react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from '../../redux/stores/store';
import { ActivitiesRenderer, Activity, activitySlice } from './Activity';
import { ActivitiesRenderer, Activity } from './Activity';
import { activitySlice } from './activitySlice';

export default {
title: 'Activity',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/activity/Activity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* limitations under the License.
*/

import { Activity, activitySlice, ActivityState } from './Activity';
import { Activity } from './Activity';
import { activitySlice, ActivityState } from './activitySlice';

const { reducer, actions } = activitySlice;
const { launchActivity, close, update } = actions;
Expand Down
93 changes: 1 addition & 92 deletions frontend/src/components/activity/Activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
useMediaQuery,
useTheme,
} from '@mui/material';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { clamp, throttle } from 'lodash';
import React, {
createContext,
Expand All @@ -43,6 +42,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { Trans, useTranslation } from 'react-i18next';
import { useTypedSelector } from '../../redux/hooks';
import store from '../../redux/stores/store';
import { activitySlice } from './activitySlice';

const areWindowsEnabled = false;

Expand Down Expand Up @@ -74,97 +74,6 @@ export interface Activity {
cluster?: string;
}

export interface ActivityState {
/** History of opened activites, list of IDs */
history: string[];
/** Map of all open activities, key is the ID */
activities: Record<string, Activity>;
}

const initialState: ActivityState = {
history: [],
activities: {},
};

export const activitySlice = createSlice({
name: 'activity',
initialState,
reducers: {
launchActivity(state, action: PayloadAction<Activity>) {
// Add to history
if (!action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
state.history.push(action.payload.id);
}

// Close other temporary tabs
Object.values(state.activities).forEach(activity => {
if (activity.temporary) {
delete state.activities[activity.id];
state.history = state.history.filter(it => it !== activity.id);
}
});

if (!state.activities[action.payload.id]) {
// New activity, add it to the state
state.activities[action.payload.id] = action.payload;
} else {
// Existing activity, un-minimize it
state.activities[action.payload.id].minimized = false;
}

// Make it fullscreen on small windows
if (window.innerWidth < 1280) {
state.activities[action.payload.id] = {
...state.activities[action.payload.id],
location: 'full',
};
}

// Dispatch resize event so the content adjusts
// 200ms delay for animations
setTimeout(() => {
window?.dispatchEvent?.(new Event('resize'));
}, 200);
},
close(state, action: PayloadAction<string>) {
// Remove the activity from history
state.history = state.history.filter(it => it !== action.payload);
// Remove from state
delete state.activities[action.payload];
},
update(state, action: PayloadAction<Partial<Activity> & { id: string }>) {
// Bump this activity in history
if (!action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
state.history.push(action.payload.id);
}

// Remove from history it it's minimized
if (action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
}

// Update the state
state.activities[action.payload.id] = {
...state.activities[action.payload.id],
...action.payload,
};

// Dispatch resize event so the content adjusts
// 200ms delay for animations
setTimeout(() => {
window?.dispatchEvent?.(new Event('resize'));
}, 200);
},
reset() {
return initialState;
},
},
});

export const activityReducer = activitySlice.reducer;

export const Activity = {
/** Launches new Activity */
launch(activity: Activity) {
Expand Down
109 changes: 109 additions & 0 deletions frontend/src/components/activity/activitySlice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { Activity } from './Activity';

export interface ActivityState {
/** History of opened activities, list of IDs */
history: string[];
/** Map of all open activities, key is the ID */
activities: Record<string, Activity>;
}

const initialState: ActivityState = {
history: [],
activities: {},
};

export const activitySlice = createSlice({
name: 'activity',
initialState,
reducers: {
launchActivity(state, action: PayloadAction<Activity>) {
// Add to history
if (!action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
state.history.push(action.payload.id);
}

// Close other temporary tabs
Object.values(state.activities).forEach(activity => {
if (activity.temporary) {
delete state.activities[activity.id];
state.history = state.history.filter(it => it !== activity.id);
}
});

if (!state.activities[action.payload.id]) {
// New activity, add it to the state
state.activities[action.payload.id] = action.payload;
} else {
// Existing activity, un-minimize it
state.activities[action.payload.id].minimized = false;
}

// Make it fullscreen on small windows
if (window.innerWidth < 1280) {
state.activities[action.payload.id] = {
...state.activities[action.payload.id],
location: 'full',
};
}

// Dispatch resize event so the content adjusts
// 200ms delay for animations
setTimeout(() => {
window?.dispatchEvent?.(new Event('resize'));
}, 200);
},
close(state, action: PayloadAction<string>) {
// Remove the activity from history
state.history = state.history.filter(it => it !== action.payload);
// Remove from state
delete state.activities[action.payload];
},
update(state, action: PayloadAction<Partial<Activity> & { id: string }>) {
// Bump this activity in history
if (!action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
state.history.push(action.payload.id);
}

// Remove from history if it's minimized
if (action.payload.minimized) {
state.history = state.history.filter(it => it !== action.payload.id);
}

// Update the state
state.activities[action.payload.id] = {
...state.activities[action.payload.id],
...action.payload,
};

// Dispatch resize event so the content adjusts
// 200ms delay for animations
setTimeout(() => {
window?.dispatchEvent?.(new Event('resize'));
}, 200);
},
reset() {
return initialState;
},
},
});

export const activityReducer = activitySlice.reducer;
6 changes: 6 additions & 0 deletions frontend/src/plugin/__snapshots__/pluginLib.snapshot
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"Activity": {
"close": [Function],
"launch": [Function],
"reset": [Function],
"update": [Function],
},
"ApiProxy": {
"ApiError": [Function],
"apiFactory": [Function],
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as ReactRedux from 'react-redux';
import * as ReactRouter from 'react-router-dom';
import * as Recharts from 'recharts';
import semver from 'semver';
import { Activity } from '../components/activity/Activity';
import { runCommand } from '../components/App/runCommand';
import { themeSlice } from '../components/App/themeSlice';
import * as CommonComponents from '../components/common';
Expand Down Expand Up @@ -100,6 +101,7 @@ window.pluginLib = {
Plugin,
useTranslation,
...registryToExport,
Activity,
};

// backwards compat.
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/redux/reducers/reducers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { combineReducers } from 'redux';
import { activityReducer } from '../../components/activity/Activity';
import { activityReducer } from '../../components/activity/activitySlice';
import notificationsReducer from '../../components/App/Notifications/notificationsSlice';
import themeReducer from '../../components/App/themeSlice';
import graphViewReducer from '../../components/resourceMap/graphViewSlice';
Expand Down
3 changes: 2 additions & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"src/lib/util.ts",
"src/lib/AppTheme.ts",
"src/plugin/lib.ts",
"src/plugin/registry.tsx"
"src/plugin/registry.tsx",
"src/components/activity/Activity.tsx"
],
"entryPointStrategy": "expand",
"excludeExternals": true,
Expand Down
1 change: 1 addition & 0 deletions plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ There you will see detailed API documentation, examples, and guides on how to de
| Folder | Description |
| ------------------------------------------------------ | --------------------------------------------- |
| [examples/](examples) | Examples folder. |
| [examples/activity](examples/activity) | Create Activities (popup windows). |
| [examples/app-menus](examples/app-menus) | Add app window menus. |
| [examples/change-logo](examples/change-logo) | Change the logo. |
| [examples/cluster-chooser](examples/cluster-chooser) | Override default chooser button. |
Expand Down
29 changes: 29 additions & 0 deletions plugins/examples/activity/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# The output for npm run storybook-book, static html built storybook for the plugin
storybook-static

.eslintcache

5 changes: 5 additions & 0 deletions plugins/examples/activity/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
17 changes: 17 additions & 0 deletions plugins/examples/activity/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
Loading
Loading