hyperdx/packages/api/src/api-app.ts
Brandon Pereira 5885d47964
[HDX-2300] introduce Shared Filters for team-wide filter visibility and discoverability (#2047)
## Summary

Introduces a "Shared Filters" feature (in addition to locally pinned items in HyperDX.

Especially helpful for teams with lots of filters and team members, allows users to highlight the top filters easily for all members.

This has been one of the most requested features we have received from enterprise customers. \

> **Note:** - currently any user on a team can modify shared filters - we may want/need to introduce role limits to this, but that is oos

### Screenshots or video

https://github.com/user-attachments/assets/9613d37c-d8d6-4aeb-9e47-1ad25532a862


### How to test locally or on Vercel

1. Start the dev server (`yarn dev`)
2. Navigate to the Search page
3. Pin a filter field using the 📌 icon on any filter group header —you should be asked to pin (existing) or add to shared filters.
4. Share a specific value by hovering over a filter checkbox row and clicking the pin icon — it should also appear in Shared Filters
5. Reload the page — pins should persist (MongoDB-backed)
6. Open a second browser/incognito window with the same team — pins should be visible there too
7. Click the ⚙ gear icon next to "Filters" — toggle "Show Shared Filters" off/on
8. Click "Reset Shared Filters" in the gear popover to clear all team pins

### References

- Linear Issue: https://linear.app/clickhouse/issue/HDX-2300/sailpoint-neara-global-filter-pinning
- Related PRs: Previous WIP branch `brandon/shared-filters-ui` (superseded by this implementation)
2026-04-16 21:28:52 +00:00

135 lines
4.9 KiB
TypeScript

import compression from 'compression';
import MongoStore from 'connect-mongo';
import express from 'express';
import session from 'express-session';
import onHeaders from 'on-headers';
import * as config from './config';
import mcpRouter from './mcp/app';
import { isUserAuthenticated } from './middleware/auth';
import defaultCors from './middleware/cors';
import { appErrorHandler } from './middleware/error';
import routers from './routers/api';
import clickhouseProxyRouter from './routers/api/clickhouseProxy';
import connectionsRouter from './routers/api/connections';
import favoritesRouter from './routers/api/favorites';
import pinnedFiltersRouter from './routers/api/pinnedFilters';
import savedSearchRouter from './routers/api/savedSearch';
import sourcesRouter from './routers/api/sources';
import externalRoutersV2 from './routers/external-api/v2';
import usageStats from './tasks/usageStats';
import logger, { expressLogger } from './utils/logger';
import passport from './utils/passport';
const app: express.Application = express();
const sess: session.SessionOptions & { cookie: session.CookieOptions } = {
// Use a slot-specific cookie name in dev so multiple worktrees on localhost
// don't overwrite each other's session cookies.
...(config.IS_DEV && process.env.HDX_DEV_SLOT
? { name: `connect.sid.${process.env.HDX_DEV_SLOT}` }
: {}),
resave: false,
saveUninitialized: false,
secret: config.EXPRESS_SESSION_SECRET,
cookie: {
secure: false,
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
},
rolling: true,
store: new MongoStore({ mongoUrl: config.MONGO_URI }),
};
app.set('trust proxy', 1);
if (!config.IS_CI && config.FRONTEND_URL) {
const feUrl = new URL(config.FRONTEND_URL);
sess.cookie.domain = feUrl.hostname;
if (feUrl.protocol === 'https:') {
sess.cookie.secure = true;
}
}
app.disable('x-powered-by');
app.use(compression());
app.use(express.json({ limit: '32mb' }));
app.use(express.text({ limit: '32mb' }));
app.use(express.urlencoded({ extended: false, limit: '32mb' }));
app.use(session(sess));
if (!config.IS_LOCAL_APP_MODE) {
app.use(passport.initialize());
app.use(passport.session());
}
if (!config.IS_CI) {
app.use(expressLogger);
}
// Allows timing data from frontend package
// see: https://github.com/expressjs/cors/issues/102
app.use(function (req, res, next) {
onHeaders(res, function () {
const allowOrigin = res.getHeader('Access-Control-Allow-Origin');
if (allowOrigin) {
res.setHeader('Timing-Allow-Origin', allowOrigin);
}
});
next();
});
app.use(defaultCors);
// ---------------------------------------------------------------------
// ----------------------- Background Jobs -----------------------------
// ---------------------------------------------------------------------
if (config.USAGE_STATS_ENABLED && !config.IS_CI) {
usageStats();
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ----------------------- Internal Routers ----------------------------
// ---------------------------------------------------------------------
// PUBLIC ROUTES
app.use('/', routers.rootRouter);
// SELF-AUTHENTICATED ROUTES (validated via access key, not session middleware)
app.use('/mcp', mcpRouter);
// PRIVATE ROUTES
app.use('/ai', isUserAuthenticated, routers.aiRouter);
app.use('/alerts', isUserAuthenticated, routers.alertsRouter);
app.use('/dashboards', isUserAuthenticated, routers.dashboardRouter);
app.use('/me', isUserAuthenticated, routers.meRouter);
app.use('/team', isUserAuthenticated, routers.teamRouter);
app.use('/webhooks', isUserAuthenticated, routers.webhooksRouter);
app.use('/connections', isUserAuthenticated, connectionsRouter);
app.use('/sources', isUserAuthenticated, sourcesRouter);
app.use('/saved-search', isUserAuthenticated, savedSearchRouter);
app.use('/favorites', isUserAuthenticated, favoritesRouter);
app.use('/pinned-filters', isUserAuthenticated, pinnedFiltersRouter);
app.use('/clickhouse-proxy', isUserAuthenticated, clickhouseProxyRouter);
// ---------------------------------------------------------------------
// TODO: Separate external API routers from internal routers
// ---------------------------------------------------------------------
// ----------------------- External Routers ----------------------------
// ---------------------------------------------------------------------
// API v2
// Only initialize Swagger in development or if explicitly enabled
if (
process.env.NODE_ENV !== 'production' &&
process.env.ENABLE_SWAGGER === 'true'
) {
// Will require a refactor to ESM to use import statements
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { setupSwagger } = require('./utils/swagger');
setupSwagger(app);
logger.info('Swagger UI setup and available at /api/v2/docs');
}
app.use('/api/v2', externalRoutersV2);
// error handling
app.use(appErrorHandler);
export default app;