Our blazing fast Grid component built with pure JavaScript


Post by branden »

Hey team,

I am using a Gantt and Scheduler Pro instance on the same page, partnered together to show the same data in sync with eachother. I have configured a single ProjectModel as such:

const projectModel = new ProjectModel({
        silenceInitialCommit: false,
        autoLoad: true,
        autoSync: true,
        allowNoId: false,
        taskModelClass: ProductModel,
        dependencyStore: { writeAllFields: true },
        assignmentStore: { writeAllFields: true },
        transport: {
            load: { url: '/api/production/get-product-schedule' },
            sync: { url: '/api/production/sync-product-schedule' }
        },
        stm: { autoRecord: true }
    }

and passed the reference to both of them.

However, on initial load, both the Scheduler and Gantt instances call the load URL, which fetches the same data (tasks, resources, deps, etc.). There are quite a lot of tasks in our gantt and loading this takes some time, slowing down the initial render. Is it possible to only load that data once for both components, since the data is identical?


Post by alex.l »

I don't see that problem in our SchedulerPro + Gantt demo https://bryntum.com/examples/gantt/gantt-schedulerpro/

Could you please share your app or give us steps to reproduce using our demo?

All the best,
Alex


Post by branden »

Hey alex,

After further investigation the bug is just due to the way the react lifecycle is triggering renders. I'm using Next.js, so at the top level we have:

function Planner({ functions, user, navdata, silenceInitialCommit }) {
    // Since SSR disabled data renders at indeterminate times, setting the components partner property requires this
    // async retry function.
    const addPartner = useCallback((node) => {
        if (ganttRef.current === null) setTimeout(addPartner, 1000, node);
        else node.instance.addPartner(ganttRef.current.instance);
    }, []);

    const ganttRef = useRef(null);
    const [schedulerRef, setSchedulerRef] = useState(null);
    const onSchedulerChange = useCallback(node => {
        if (node !== null) {
            setSchedulerRef(node);
            addPartner(node);
        }
    }, [addPartner]); // adjust deps

    return (
        <Layout head='Product Planner' bannerText='Product Planner' className={styles.noLabel} navdata={navdata}>
            <Gantt
                silenceInitialCommit={silenceInitialCommit}
                readOnly={!functions.includes('edit_schedule')}
                taskDragFeature={functions.includes('edit_schedule')}
                taskCopyPasteFeature={false}
                id='gantt-planner'
                ganttRef={ganttRef}
                schedulerRef={onSchedulerChange}
                subGridConfigs={{ locked: { width: 450 }}}
                filterFeature
                panFeature
                infiniteScroll
                cellEditFeature={{ addNewAtEnd: false }}
                startDate={new Date(new Date().setHours(4)).toISOString()}
                dependencyEditFeature
                fillLastColumn={false}
                timeRangesFeature={{ showCurrentTimeLine: true }}
                stripeFeature
                labelsFeature={{
                    left: {
                        field: 'name',
                        editor: {
                            type: 'textfield'
                        }
                    }
                }}
                tbar={{ type: 'producttoolbar' }}
                columns={[
                    { text: 'ID', field: 'id', editor: false, hidden: true },
                    { type: 'name', text: 'Product', field: 'name', width: 300 },
                    { text: 'Assigned', type: 'resourceassignment', showAvatars: true },
                    { text: 'Workorder', field: 'workorderId', editor: false },
                    { text: 'Work Centre', field: 'workCentre', editor: false },
                    { text: 'Customer', field: 'customer', editor: false },
                    { type: 'startdate', field: 'startDate' },
                    { text: 'Qty', field: 'progress', editor: false },
                    { type: 'percent', text: 'Progress', field: 'percentDone', editor: false },
                    { text: 'PO', field: 'poNo', editor: false },
                    { type: 'date', format: 'll', text: 'Due Date', field: 'dueDate', editor: false },
                ]}
            />
        </Layout>
    );
}

The weird timeout to add the components as partners is necessary because the Gantt and Scheduler wrappers need to be loaded dynamically (no SSR), and can finish loading at different times. So setting the parent needs to be handled in this way to ensure that both components have loaded before linking them.

This causes React to trigger a redraw when this happens, so this causes the ProjectModel in the wrapper to be created twice, causing the load to be called twice. My wrapper looks like so:

export class ProductModel extends TaskModel {
    static get fields() {
        return [
            { label: 'ID', name: 'id' },
            { label: 'Product', name: 'name', alwaysWrite: true, tree: true },
            { name: 'startDate', alwaysWrite: true },
            { name: 'endDate', alwaysWrite: true },
            { name: 'releasesId' },
            { name: 'dueDate'},
            { label: 'Work Centre', name: 'workCentre' },
            { label: 'Work Centre ID', name: 'workCentreId', alwaysWrite: true },
            // { label: 'Calendar', name: 'calendar', alwaysWrite: true },
            { label: 'Note', name: 'note', alwaysWrite: true },
            { name: 'duration', alwaysWrite: true },
            { name: 'parentIndex', alwaysWrite: true },
            { name: 'parentId', alwaysWrite: true },
            { name: 'qaReady' },
            { name: 'isParent' },
            { name: 'schedulingMode' },
            { name: 'effort', alwaysWrite: true },
        ]
    }
}

export default function ProductGantt({ ganttRef, schedulerRef, silenceInitialCommit, ...props }) {
    const productConfig = useRef(new ProjectModel({
        silenceInitialCommit: silenceInitialCommit,
        autoLoad: true,
        autoSync: true,
        allowNoId: false,
        taskModelClass: ProductModel,
        dependencyStore: { writeAllFields: true },
        assignmentStore: { writeAllFields: true },
        transport: {
            load: { url: '/api/production/get-product-schedule' },
            sync: { url: '/api/production/sync-product-schedule' }
        },
        stm: { autoRecord: true }
    }));

    return (
        <Fragment>
            <BryntumGantt
                {...props}
                ref={ganttRef}
                project={productConfig.current}
                viewPreset='weekAndDay'
                rowReorderFeature={false}
                onBeforeEventDelete={({ eventRecords }) => {
                    for (const record of eventRecords) {
                        if (!record.isParent) {
                            return false;
                        }
                    }
                }}
                taskMenuFeature={{
                    items: {
                        deleteTask: false,
                        filterStringEquals: false,
                        convertToMilestone: false,
                        indent: false,
                        outdent: false,
                        add: false,
                    }
                }}
                loadMask='Auto-Scheduling Tasks...'
            />
            <BryntumSplitter />
            <BryntumSchedulerPro
                resourceImagePath={'users/'}
                rowReorderFeature
                eventDragCreateFeature={false}
                eventCreateFeature={false}
                eventCopyPasteFeature={false}
                resourceNonWorkingTimeFeature
                panFeature
                ref={schedulerRef}
                project={productConfig.current}
                height={600}
                eventMenuFeature={{
                    items: {
                        add: false
                    }
                }}
                columns={[
                    { type: 'resourceInfo', field: 'name', text: 'Employee', showEventCount: false, editor: false, width: 200 },
                    { text: 'Tasks', field: 'events.length', editor: false, width: 100, },
                    // { text: 'Groups', field: 'groups', editor: false, width: 100, },
                    {
                        text     : 'Assigned hours',
                        width    : 160,
                        editor   : false,
                        renderer : ({ record }) => record.events.map(task => task.duration).reduce((total, current) => {
                            return total + current;
                        }, 0).toFixed(1) + ' hours'
                    }
                ]}
            />
        </Fragment>
    )
}

The issue arises because I want to dynamically set the silenceInitialCommit flag, based on whether entries were added into the database in the back-end. I need to let chronograph calculate the new values in the front-end and update them if so, and I'd like to avoid that if there's no need to.

Creating the ProjectModel outside of the component doesn't allow me to change the silenceInitialCommit flag after its been set, and I need to access the props within the component to determine what its value should be.

I've tried adding in a ref, or a useState or useEffect hook, but nothing seems to fix the issue. Any thoughts?


Post by alex.l »

The issue arises because I want to dynamically set the silenceInitialCommit flag, based on whether entries were added into the database in the back-end. I need to let chronograph calculate the new values in the front-end and update them if so, and I'd like to avoid that if there's no need to.

If there is nothing to commit (all calculated and saved before), silenceInitialCommit: false should not trigger any extra data saving, I am not sure why do you need to update that dynamically at all. Maybe you're trying to make a workaround for our bug. Please comment this moment and if you still have a request - we need steps to reproduce that.

Anyway, I have two ideas to share:

  1. https://reactjs.org/docs/context.html - this solution will be working here. You'll need to wrap your component into context, and define project model as a part of that context. You'll still be allowed to create project model at the moment you need.
  2. I didn't test, because I don't have a test case, but in theory that should work here.
    // declare var out of component
    let project; 
    
    export default function ProductGantt({ ganttRef, schedulerRef, silenceInitialCommit, ...props }) {
        // check if project already created
        project = project || new ProjectModel({ silenceInitialCommit, ....... });
    
    

All the best,
Alex


Post by branden »

Right, a context provider is probably the Reactive solution to the issue. I will look into that for the time being.

However, you say silenceInitialCommit should automatically compare the post-propagation values with what's received from the database, and if nothing was changed the commit should abort? If so, that would remove my need to keep track of that myself. As it exists though, I could:

  1. Insert some records into the database from the back-end (not accounting for things like non-
    working time, calendars, etc.).
  2. Open my web page containing the Gantt component, loading the data from the database.
  3. The Chronograph engine updates tasks to honor dependencies and calendars and writes the entire dataset back to the server, containing the updated values.
  4. Use the Gantt as normal.

If I do this, and then repeat steps 2-4, the initial commit still writes the whole dataset to the server on load, even if no data has changed since the last time the Gantt was opened. Is this the expected behaviour for this use case?


Post by alex.l »

It is not expected behaviour. If all data has been saved in previous initial commit, project.changes will be empty after next reload and no request will be triggered. If you have it triggered, please check if you saved all data without changes in dates or other scheduling sensitive data. If all good and you have commit triggered anyway, that sounds like a bug. Might be good to get a test case to reproduce that for debugging.

All the best,
Alex


Post Reply